Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON Doc Editor / Locale file editor #370

Merged
merged 10 commits into from
Sep 5, 2021
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = {
],
globals: {
$$BUILD: 'readonly',
fetch: 'readonly',
},
rules: {
'jsx-a11y/no-noninteractive-element-interactions': ['off'], // We intend to enable this once we refactor certain key components.
Expand Down
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ For detailed rules of this file, see [Changelog Rules](#changelog-rules)
## [Unreleased][]

### ✨ Added
*
* A new page to assist with editing QMS locale files has been added. - [#370][]
* A general purpose JSON object editor has been implemented. this should come in handy at some point! - [#370][]

### ⚡ Changed
* Error readout for unknown API Errors has been improved. - [#328][]
Expand All @@ -21,7 +22,7 @@ For detailed rules of this file, see [Changelog Rules](#changelog-rules)
* Moved all custom server functions into suitable replacements provided by our site framework. - [#329][]
* This move has let us drop our entire custom backend.
* Updated `<Switch />` with an improved loading state animation. - [#330][]
* Other smaller changes to sreamline development. - [#328][], [#329][]
* Other smaller changes to sreamline development. - [#328][], [#329][], [#370][]


### 🐛 Fixed
Expand All @@ -39,6 +40,7 @@ For detailed rules of this file, see [Changelog Rules](#changelog-rules)
[#329]: https://github.com/FuelRats/fuelrats.com/pull/329
[#330]: https://github.com/FuelRats/fuelrats.com/pull/330
[#333]: https://github.com/FuelRats/fuelrats.com/pull/333
[#370]: https://github.com/FuelRats/fuelrats.com/pull/370
[Unreleased]: https://github.com/FuelRats/fuelrats.com/compare/v2.13.0...HEAD


Expand Down
47 changes: 47 additions & 0 deletions src/components/JsonEditor/JsonEditor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import PropTypes from 'prop-types'

import JsonObject from '~/components/JsonEditor/JsonObject'
import useForm from '~/hooks/useForm'

import styles from './JsonEditor.module.scss'




// Component Constants





function JsonEditor (props) {
const {
className,
data,
} = props

const { Form, state } = useForm({ data })

return (
<>
<Form className={[styles.jsonEditor, className]}>
<JsonObject name="object" node={data} />
</Form>
<br />
<pre>
{JSON.stringify(state, null, ' ')}
</pre>
</>
)
}

JsonEditor.propTypes = {
className: PropTypes.string,
data: PropTypes.object.isRequired,
}





export default JsonEditor
42 changes: 42 additions & 0 deletions src/components/JsonEditor/JsonEditor.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@import '../../scss/fonts';
@import '../../scss/colors';

.jsonEditor {
padding: 0.5rem 1rem;

border: 1px solid $black;

background: rgba($grey, 0.25);
}

.jsonObject {
padding: 0.5rem 0;
}

.hasDepth {
padding-left: 2rem;
}

.jsonField {
display: flex;

flex-direction: row;
align-items: center;

padding: 0.5rem 0 0.5rem 2rem;
}

.fieldInput {
display: inline-block;
}


.fieldLabel {
display: inline-block;
position: relative;


padding-right: 0.75rem;

font-family: $monospace-fonts;
}
43 changes: 43 additions & 0 deletions src/components/JsonEditor/JsonField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useCallback } from 'react'

import { useField } from '~/hooks/useForm'

import styles from './JsonEditor.module.scss'



export default function JsonField (props) {
const {
node,
name,
path,
depth,
} = props
const labelId = `${path}-label`
const inputId = `${path}-input`

const {
value = '',
handleChange,
} = useField(path)

const onChange = useCallback((event) => {
handleChange(event, event.target.value === '' ? node : undefined)
}, [handleChange, node])

return (
<div className={['jsonField', styles.jsonField, { [styles.hasDepth]: depth > 0 }]}>
<label className={styles.fieldLabel} htmlFor={inputId} id={labelId} title={node}>
{`"${name}":`}
</label>

<input
aria-labelledby={labelId}
className={styles.fieldInput}
id={inputId}
type="text"
value={value === node ? '' : value}
onChange={onChange} />
</div>
)
}
47 changes: 47 additions & 0 deletions src/components/JsonEditor/JsonObject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import _isPlainObject from 'lodash/isPlainObject'

import JsonField from '~/components/JsonEditor/JsonField'

import styles from './JsonEditor.module.scss'

function getComponent (value) {
if (_isPlainObject(value)) {
return JsonObject
}

return JsonField
}

export default function JsonObject (props) {
const {
node,
name,
depth = 0,
path,
hasNext,
} = props
const propDepth = depth + 1
const nodeLength = Object.keys(node).length
return (
<div className={['jsonObject', styles.jsonObject, { [styles.hasDepth]: depth > 0 }]}>
<span className={styles.fieldLabel}>{`${depth ? `"${name}": ` : ''}{`}</span>
{
Object.entries(node).map(([propName, propNode], idx) => {
const Component = getComponent(propNode)
const propPath = depth ? `${path}.${propName}` : propName

return (
<Component
key={propPath}
depth={propDepth}
hasNext={idx < nodeLength}
name={propName}
node={propNode}
path={propPath} />
)
})
}
<span className={styles.fieldLabel}>{`}${hasNext ? ',' : ''}`}</span>
</div>
)
}
1 change: 1 addition & 0 deletions src/components/JsonEditor/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './JsonEditor'
5 changes: 5 additions & 0 deletions src/data/apiErrorLocalisations.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ const apiErrorLocalisations = {
title: 'Internal Server Error',
detail: 'The server encountered an unexpected condition that prevented it from fulfilling the request.',
},

not_found: {
title: 'Not Found',
detail: 'The origin server did not find a current representation for the target resource or is not willing to disclose that one exists.',
},
}

export default apiErrorLocalisations
38 changes: 38 additions & 0 deletions src/pages/api/qms/locales/[id].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { listLocaleFiles } from '~/pages/api/qms/locales'
import { NotFoundAPIError } from '~/util/server/errors'
import acceptMethod from '~/util/server/middleware/acceptMethod'
import jsonApiRoute from '~/util/server/middleware/jsonApiRoute'

const maxAge = 21600000 // 6 hours

export function getLocaleData (ctx, localeMeta) {
return ctx.fetch({
url: localeMeta.links.related,
cache: {
key: `qms-locale-data-${localeMeta.id}`,
maxAge,
},
}).then(({ data }) => {
return data
})
}

export default jsonApiRoute(
acceptMethod.GET(),
async (ctx) => {
const localeMeta = (await listLocaleFiles(ctx)).find((data) => {
return data.id === ctx.req.query.id
})
if (!localeMeta) {
throw new NotFoundAPIError({ parameter: 'id' })
}

ctx.send({
id: localeMeta.id,
type: 'locale-data',
attributes: {
data: await getLocaleData(ctx, localeMeta),
},
})
},
)
32 changes: 32 additions & 0 deletions src/pages/api/qms/locales/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import acceptMethod from '~/util/server/middleware/acceptMethod'
import jsonApiRoute from '~/util/server/middleware/jsonApiRoute'

const maxAge = 21600000 // 6 hours

export function listLocaleFiles (ctx) {
return ctx.fetch({
url: 'https://api.github.com/repos/fuelrats/QMS/contents/qms/frontend/public/locales/en?ref=main',
cache: {
key: 'qms-locale-list',
maxAge,
},
}).then(({ data }) => {
return data.map((fileMeta) => {
return {
id: fileMeta.name,
type: 'locale-files',
links: {
self: fileMeta.url,
related: fileMeta.download_url,
},
}
})
})
}

export default jsonApiRoute(
acceptMethod.GET(),
async (ctx) => {
ctx.send(await listLocaleFiles(ctx))
},
)
Loading