Files
shields/frontend/components/customizer/query-string-builder.tsx
dependabot[bot] 271547d2c6 chore(deps): bump query-string from 7.1.3 to 8.0.3 (#8728)
* chore(deps): bump query-string from 7.1.3 to 8.0.3

Bumps [query-string](https://github.com/sindresorhus/query-string) from 7.1.3 to 8.0.3.
- [Release notes](https://github.com/sindresorhus/query-string/releases)
- [Commits](https://github.com/sindresorhus/query-string/compare/v7.1.3...v8.0.3)

---
updated-dependencies:
- dependency-name: query-string
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* update import

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: chris48s <git@chris-shaw.dev>
2022-12-19 18:53:21 +00:00

349 lines
9.3 KiB
TypeScript

import React, {
useState,
useEffect,
ChangeEvent,
ChangeEventHandler,
} from 'react'
import styled from 'styled-components'
import humanizeString from 'humanize-string'
import qs from 'query-string'
import { advertisedStyles } from '../../lib/supported-features'
import { noAutocorrect, StyledInput } from '../common'
import {
BuilderContainer,
BuilderLabel,
BuilderCaption,
} from './builder-common'
const QueryParamLabel = styled(BuilderLabel)`
margin: 5px;
`
const QueryParamInput = styled(StyledInput)`
margin: 5px 10px;
`
const QueryParamCaption = styled(BuilderCaption)`
margin: 5px;
`
type BadgeOptionName = 'style' | 'label' | 'color' | 'logo' | 'logoColor'
interface BadgeOptionInfo {
name: BadgeOptionName
label?: string
shieldsDefaultValue?: string
}
const supportedBadgeOptions = [
{ name: 'style', shieldsDefaultValue: 'flat' },
{ name: 'label', label: 'override label' },
{ name: 'color', label: 'override color' },
{ name: 'logo', label: 'named logo' },
{ name: 'logoColor', label: 'override logo color' },
] as BadgeOptionInfo[]
function getBadgeOption(name: BadgeOptionName): BadgeOptionInfo {
const result = supportedBadgeOptions.find(opt => opt.name === name)
if (!result) {
throw Error(`Unknown badge option: ${name}`)
}
return result
}
function getQueryString({
queryParams,
badgeOptions,
}: {
queryParams: Record<string, string | boolean>
badgeOptions: Record<BadgeOptionName, string | undefined>
}): {
queryString: string
isComplete: boolean
} {
// Use `string | null`, because `query-string` renders e.g.
// `{ compact_message: null }` as `?compact_message`. This is
// what we want for boolean params that are true (see below).
const outQuery = {} as Record<string, string | null>
let isComplete = true
Object.entries(queryParams).forEach(([name, value]) => {
// As above, there are two types of supported params: strings and
// booleans.
if (typeof value === 'string') {
if (value) {
outQuery[name] = value.trim()
} else {
// Skip empty params.
isComplete = false
}
} else {
// Generate empty query params for boolean parameters by translating
// `{ compact_message: true }` to `?compact_message`. When values are
// false, skip the param.
if (value) {
outQuery[name] = null
}
}
})
Object.entries(badgeOptions).forEach(([name, value]) => {
const { shieldsDefaultValue } = getBadgeOption(name as BadgeOptionName)
if (value && value !== shieldsDefaultValue) {
outQuery[name] = value
}
})
const queryString = qs.stringify(outQuery)
return { queryString, isComplete }
}
function ServiceQueryParam({
name,
value,
exampleValue,
isStringParam,
stringParamCount,
handleServiceQueryParamChange,
}: {
name: string
value: string | boolean
exampleValue: string
isStringParam: boolean
stringParamCount?: number
handleServiceQueryParamChange: ChangeEventHandler<HTMLInputElement>
}): JSX.Element {
return (
<tr>
<td>
<QueryParamLabel htmlFor={name}>
{humanizeString(name).toLowerCase()}
</QueryParamLabel>
</td>
<td>
{isStringParam && (
<QueryParamCaption>
{stringParamCount === 0 ? `e.g. ${exampleValue}` : exampleValue}
</QueryParamCaption>
)}
</td>
<td>
{isStringParam ? (
<QueryParamInput
name={name}
onChange={handleServiceQueryParamChange}
type="text"
value={value as string}
{...noAutocorrect}
/>
) : (
<input
checked={value as boolean}
name={name}
onChange={handleServiceQueryParamChange}
type="checkbox"
/>
)}
</td>
</tr>
)
}
function BadgeOptionInput({
name,
value,
handleBadgeOptionChange,
}: {
name: BadgeOptionName
value: string
handleBadgeOptionChange: ChangeEventHandler<
HTMLSelectElement | HTMLInputElement
>
}): JSX.Element {
if (name === 'style') {
return (
<select name="style" onChange={handleBadgeOptionChange} value={value}>
{advertisedStyles.map(style => (
<option key={style} value={style}>
{style}
</option>
))}
</select>
)
} else {
return (
<QueryParamInput
name={name}
onChange={handleBadgeOptionChange}
type="text"
value={value}
{...noAutocorrect}
/>
)
}
}
function BadgeOption({
name,
value,
handleBadgeOptionChange,
}: {
name: BadgeOptionName
value: string
handleBadgeOptionChange: ChangeEventHandler<HTMLInputElement>
}): JSX.Element {
const {
label = humanizeString(name),
shieldsDefaultValue: hasShieldsDefaultValue,
} = getBadgeOption(name)
return (
<tr>
<td>
<QueryParamLabel htmlFor={name}>{label}</QueryParamLabel>
</td>
<td>
{!hasShieldsDefaultValue && (
<QueryParamCaption>optional</QueryParamCaption>
)}
</td>
<td>
<BadgeOptionInput
handleBadgeOptionChange={handleBadgeOptionChange}
name={name}
value={value}
/>
</td>
</tr>
)
}
// The UI for building the query string, which includes two kinds of settings:
// 1. Custom query params defined by the service, stored in
// `this.state.queryParams`
// 2. The standard badge options which apply to all badges, stored in
// `this.state.badgeOptions`
export default function QueryStringBuilder({
exampleParams,
initialStyle = 'flat',
onChange,
}: {
exampleParams: { [k: string]: string }
initialStyle?: string
onChange: ({
queryString,
isComplete,
}: {
queryString: string
isComplete: boolean
}) => void
}): JSX.Element {
const [queryParams, setQueryParams] = useState(() =>
// For each of the custom query params defined in `exampleParams`,
// create empty values in `queryParams`.
Object.entries(exampleParams)
.filter(
// If the example defines a value for one of the standard supported
// options, do not duplicate the corresponding parameter.
([name]) => !supportedBadgeOptions.some(option => name === option.name)
)
.reduce((accum, [name, value]) => {
// Custom query params are either string or boolean. Inspect the example
// value to infer which one, and set empty values accordingly.
// Throughout the component, these two types are supported in the same
// manner: by inspecting this value type.
const isStringParam = typeof value === 'string'
accum[name] = isStringParam ? '' : true
return accum
}, {} as { [k: string]: string | boolean })
)
// For each of the standard badge options, create empty values in
// `badgeOptions`. When `initialStyle` has been provided, use it.
const [badgeOptions, setBadgeOptions] = useState(() =>
supportedBadgeOptions.reduce((accum, { name }) => {
if (name === 'style') {
accum[name] = initialStyle
} else {
accum[name] = ''
}
return accum
}, {} as Record<BadgeOptionName, string>)
)
const handleServiceQueryParamChange = React.useCallback(
function ({
target: { name, type: targetType, checked, value },
}: ChangeEvent<HTMLInputElement>): void {
const outValue = targetType === 'checkbox' ? checked : value
setQueryParams({ ...queryParams, [name]: outValue })
},
[setQueryParams, queryParams]
)
const handleBadgeOptionChange = React.useCallback(
function ({
target: { name, value },
}: ChangeEvent<HTMLInputElement>): void {
setBadgeOptions({ ...badgeOptions, [name]: value })
},
[setBadgeOptions, badgeOptions]
)
useEffect(() => {
if (onChange) {
const { queryString, isComplete } = getQueryString({
queryParams,
badgeOptions,
})
onChange({ queryString, isComplete })
}
}, [onChange, queryParams, badgeOptions])
const hasQueryParams = Boolean(Object.keys(queryParams).length)
let stringParamCount = 0
return (
<>
{hasQueryParams && (
<BuilderContainer>
<table>
<tbody>
{Object.entries(queryParams).map(([name, value]) => {
const isStringParam = typeof value === 'string'
return (
<ServiceQueryParam
exampleValue={exampleParams[name]}
handleServiceQueryParamChange={
handleServiceQueryParamChange
}
isStringParam={isStringParam}
key={name}
name={name}
stringParamCount={
isStringParam ? stringParamCount++ : undefined
}
value={value}
/>
)
})}
</tbody>
</table>
</BuilderContainer>
)}
<BuilderContainer>
<table>
<tbody>
{Object.entries(badgeOptions).map(([name, value]) => (
<BadgeOption
handleBadgeOptionChange={handleBadgeOptionChange}
key={name}
name={name as BadgeOptionName}
value={value}
/>
))}
</tbody>
</table>
</BuilderContainer>
</>
)
}