@scenid/react-formulator 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "functions",
3
+ "description": "Cloud Functions for Firebase",
4
+ "scripts": {
5
+ "lint": "eslint .",
6
+ "serve": "firebase emulators:start --only functions",
7
+ "shell": "firebase functions:shell",
8
+ "start": "npm run shell",
9
+ "deploy": "firebase deploy --only functions",
10
+ "logs": "firebase functions:log"
11
+ },
12
+ "engines": {
13
+ "node": "16"
14
+ },
15
+ "main": "index.js",
16
+ "dependencies": {
17
+ "cors": "^2.8.5",
18
+ "eslint-config-standard": "^17.0.0",
19
+ "express": "^4.18.0",
20
+ "firebase-admin": "^10.0.2",
21
+ "firebase-functions": "^3.18.0"
22
+ },
23
+ "devDependencies": {
24
+ "eslint": "^8.9.0",
25
+ "eslint-config-google": "^0.14.0",
26
+ "firebase-functions-test": "^0.2.0"
27
+ },
28
+ "private": true
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scenid/react-formulator",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.esm.js",
6
6
  "repository": "https://dennykoch@bitbucket.org/scenid/react-formulator.git",
@@ -8,7 +8,8 @@
8
8
  "license": "UNLICENSED",
9
9
  "scripts": {
10
10
  "build": "rollup -c",
11
- "storybook": "start-storybook -p 6006",
11
+ "watch": "cross-env STORYBOOK_NODE_ENV=development start-storybook -p 6006",
12
+ "start:dev": "firebase emulators:start",
12
13
  "storybook:build": "build-storybook",
13
14
  "storybook:deploy": "yarn storybook:build && firebase deploy",
14
15
  "prepublishOnly": "yarn build",
@@ -0,0 +1,156 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import { TextField, InputAdornment, CircularProgress } from '@material-ui/core'
5
+
6
+ import Autocomplete, { createFilterOptions } from '@material-ui/lab/Autocomplete'
7
+
8
+ const filter = createFilterOptions()
9
+
10
+ const renderInput = (
11
+ {
12
+ required,
13
+ label,
14
+ onInputChange,
15
+ variant,
16
+ loading
17
+ },
18
+ params
19
+ ) => {
20
+ let InputProps = params.InputProps || {}
21
+
22
+ if (loading) {
23
+ InputProps = {
24
+ endAdornment: (
25
+ <InputAdornment position="end">
26
+ <CircularProgress
27
+ size={20}
28
+ thickness={4}
29
+ />
30
+ </InputAdornment>
31
+ )
32
+ }
33
+ }
34
+
35
+ return (
36
+ <TextField
37
+ // eslint-disable-next-line react/jsx-props-no-spreading
38
+ {...params}
39
+ required={required}
40
+ label={label}
41
+ variant={variant}
42
+ onChange={onInputChange}
43
+ InputProps={InputProps}
44
+ />
45
+ )
46
+ }
47
+
48
+ const SelectOrCreate = ({
49
+ id,
50
+ label,
51
+ variant,
52
+ loading,
53
+ required,
54
+ disabled,
55
+ allowCreate,
56
+ options,
57
+ searchKey,
58
+ renderOption,
59
+ renderNewOption,
60
+ value,
61
+ onOpen,
62
+ onClose,
63
+ onChange,
64
+ onInputChange
65
+ }) => (
66
+ <Autocomplete
67
+ id={id}
68
+ value={value}
69
+ fullWidth
70
+ disabled={disabled}
71
+ onOpen={onOpen}
72
+ onClose={onClose}
73
+ onChange={(_, newValue) => {
74
+ if (typeof newValue === 'string') {
75
+ if (!allowCreate && Array.isArray(options) && !options.find(o => o.entry === newValue)) onChange(undefined)
76
+ else onChange({ [searchKey]: newValue })
77
+ } else if (allowCreate && newValue && newValue.inputValue) {
78
+ // Create a new value from the user input
79
+ onChange({ [searchKey]: newValue.inputValue })
80
+ } else {
81
+ onChange(newValue)
82
+ }
83
+ }}
84
+ filterOptions={(allOptions, params) => {
85
+ const filtered = filter(allOptions, params)
86
+
87
+ // Suggest the creation of a new value
88
+ if (allowCreate && params.inputValue !== '') {
89
+ filtered.push({
90
+ inputValue: params.inputValue,
91
+ [searchKey]: renderNewOption(params.inputValue)
92
+ })
93
+ }
94
+
95
+ return filtered
96
+ }}
97
+ selectOnFocus
98
+ clearOnBlur
99
+ handleHomeEndKeys
100
+ options={options}
101
+ getOptionLabel={option => {
102
+ // Value selected with enter, right from the input
103
+ if (typeof option === 'string') {
104
+ if (!allowCreate && Array.isArray(options) && !options.find(o => o.entry === option)) return ''
105
+ return option
106
+ }
107
+
108
+ // Add "xxx" option created dynamically
109
+ if (option.inputValue) {
110
+ return option.inputValue
111
+ }
112
+
113
+ // Regular option
114
+ return option[searchKey]
115
+ }}
116
+ renderOption={renderOption}
117
+ freeSolo
118
+ renderInput={params => (
119
+ renderInput(
120
+ {
121
+ required,
122
+ disabled,
123
+ label,
124
+ onInputChange,
125
+ variant,
126
+ loading
127
+ },
128
+ params
129
+ )
130
+ )}
131
+ />
132
+ )
133
+
134
+ SelectOrCreate.propTypes = {
135
+ id: PropTypes.string,
136
+ label: PropTypes.string.isRequired,
137
+ variant: PropTypes.string,
138
+ loading: PropTypes.bool,
139
+ required: PropTypes.bool,
140
+ disabled: PropTypes.bool,
141
+ allowCreate: PropTypes.bool,
142
+ options: PropTypes.array.isRequired,
143
+ searchKey: PropTypes.string.isRequired,
144
+ renderOption: PropTypes.func.isRequired,
145
+ renderNewOption: PropTypes.func.isRequired,
146
+ value: PropTypes.oneOfType([
147
+ PropTypes.string,
148
+ PropTypes.object
149
+ ]).isRequired,
150
+ onOpen: PropTypes.func,
151
+ onClose: PropTypes.func,
152
+ onChange: PropTypes.func.isRequired,
153
+ onInputChange: PropTypes.func.isRequired
154
+ }
155
+
156
+ export default SelectOrCreate
@@ -0,0 +1,121 @@
1
+ import React, { useCallback } from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import { Box, Typography, FormControl, FormHelperText } from '@material-ui/core'
5
+
6
+ import FormField from '../FormField'
7
+ import FormReadOnlyText from '../../ReadOnly/FormReadOnlyText'
8
+ import SelectOrCreate from '../../Components/SelectOrCreate'
9
+
10
+ import useFetchOptions from './useFetchOptions'
11
+
12
+ const FormAutocomplete = ({
13
+ name,
14
+ label,
15
+ value,
16
+ options: optionsArg,
17
+ baererToken,
18
+ allowCreate,
19
+ dirty,
20
+ hasErrors,
21
+ errors,
22
+ variant,
23
+ required,
24
+ disabled,
25
+ readOnly,
26
+ onChange
27
+ }) => {
28
+ const { loading, fetch, abort, options } = useFetchOptions(optionsArg, { token: baererToken })
29
+
30
+ const changeValue = useCallback(newValue => { onChange({ target: { name, value: newValue?.entry || '' } }) }, [onChange])
31
+
32
+ if (readOnly) {
33
+ return (
34
+ <FormField
35
+ component={FormReadOnlyText}
36
+ type="text"
37
+ componentProps={{}}
38
+ name={name}
39
+ value={value}
40
+ />
41
+ )
42
+ }
43
+
44
+ return (
45
+ <FormControl
46
+ error={dirty && hasErrors}
47
+ margin="dense"
48
+ fullWidth
49
+ >
50
+ <SelectOrCreate
51
+ loading={loading}
52
+ label={label}
53
+ variant={variant}
54
+ allowCreate={allowCreate}
55
+ options={options}
56
+ searchKey="entry"
57
+ required={required}
58
+ disabled={disabled}
59
+ renderOption={option => (
60
+ <Box>
61
+ <Typography variant="body1">
62
+ {option.entry}
63
+ </Typography>
64
+ {
65
+ option.count !== undefined
66
+ && (
67
+ <Typography variant="caption">
68
+ {`${option.count} Einträge`}
69
+ </Typography>
70
+ )
71
+ }
72
+ </Box>
73
+ )}
74
+ renderNewOption={newValue => (
75
+ <Typography variant="body1">
76
+ <strong>{`"${newValue}" `}</strong>
77
+ erstellen
78
+ </Typography>
79
+ )}
80
+ value={value}
81
+ onOpen={() => {
82
+ changeValue({ entry: value })
83
+ fetch()
84
+ }}
85
+ onClose={abort}
86
+ onChange={changeValue}
87
+ />
88
+ {
89
+ (dirty && hasErrors)
90
+ && (
91
+ <FormHelperText>
92
+ {errors.map(e => e.message).join('. ')}
93
+ </FormHelperText>
94
+ )
95
+ }
96
+ </FormControl>
97
+ )
98
+ }
99
+
100
+ FormAutocomplete.propTypes = {
101
+ name: PropTypes.string,
102
+ label: PropTypes.string,
103
+ value: PropTypes.any,
104
+ options: PropTypes.oneOfType([
105
+ PropTypes.array,
106
+ PropTypes.string,
107
+ PropTypes.func
108
+ ]).isRequired,
109
+ baererToken: PropTypes.string,
110
+ allowCreate: PropTypes.bool,
111
+ variant: PropTypes.oneOf(['standard', 'filled', 'outlined']),
112
+ required: PropTypes.bool,
113
+ disabled: PropTypes.bool,
114
+ readOnly: PropTypes.bool,
115
+ dirty: PropTypes.bool,
116
+ hasErrors: PropTypes.bool,
117
+ errors: PropTypes.array,
118
+ onChange: PropTypes.func
119
+ }
120
+
121
+ export default FormAutocomplete
@@ -0,0 +1,83 @@
1
+ import { useCallback, useRef, useState } from 'react'
2
+
3
+ const abortableOAuthFetch = async (url, { token, signal }) => {
4
+ let r, text, result
5
+ try {
6
+ const options = {
7
+ method: 'GET'
8
+ }
9
+
10
+ if (token) {
11
+ options.headers = {
12
+ 'Content-Type': 'application/json',
13
+ Authorization: `Bearer ${token}`
14
+ }
15
+ }
16
+
17
+ if (signal) {
18
+ options.signal = signal
19
+ }
20
+
21
+ r = await fetch(url, options)
22
+
23
+ text = await r.text()
24
+ result = JSON.parse(text)
25
+ } catch (e) {
26
+ throw new Error(text)
27
+ }
28
+
29
+ if (!r.ok) throw new Error(result.error)
30
+
31
+ return result
32
+ }
33
+
34
+ const useFetchOptions = (arg, fetchOptions = {}) => {
35
+ const controller = useRef()
36
+
37
+ const [loading, setLoading] = useState(false)
38
+ const [options, setOptions] = useState(Array.isArray(arg) || [])
39
+
40
+ const fetch = useCallback(async () => {
41
+ if (Array.isArray(arg)) return setOptions(arg)
42
+ if (typeof arg !== 'string' && typeof arg !== 'function') return setOptions([])
43
+
44
+ let fetcher = arg
45
+ if (typeof arg === 'string') {
46
+ fetcher = signal => abortableOAuthFetch(arg, { token: fetchOptions.token, signal })
47
+ }
48
+
49
+ controller.current = new AbortController()
50
+ setLoading(true)
51
+
52
+ try {
53
+ const r = await fetcher(controller.current.signal)
54
+ controller.current = undefined
55
+
56
+ if (!Array.isArray(r)) throw new Error('autocomplete fetcher returned a non array value')
57
+
58
+ setOptions(r)
59
+ } catch (e) {
60
+ console.log(e)
61
+ }
62
+
63
+ setLoading(false)
64
+ }, [arg, controller.current, setLoading, setOptions])
65
+
66
+ const abort = useCallback(() => {
67
+ if (loading) setLoading(false)
68
+
69
+ if (controller.current) {
70
+ controller.current.abort()
71
+ controller.current = undefined
72
+ }
73
+ }, [loading, setLoading, controller.current])
74
+
75
+ return {
76
+ loading,
77
+ fetch,
78
+ abort,
79
+ options
80
+ }
81
+ }
82
+
83
+ export default useFetchOptions
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
4
4
  import { Checkbox, Switch } from '@material-ui/core'
5
5
 
6
6
  const isBoolean = val => val === false || val === true
7
- const isChecked = (value, defaultValue) => isBoolean(value) ? value : (defaultValue || false)
7
+ const isChecked = (value, defaultValue) => (isBoolean(value) ? value : (defaultValue || false))
8
8
 
9
9
  const FormBoolean = ({ variant, name, value, defaultValue, needsToBeTrue, onChange }) => {
10
10
  const handleChange = e => {
@@ -0,0 +1,29 @@
1
+ import React from 'react'
2
+ import PropTypes from 'prop-types'
3
+
4
+ import FormAutocomplete from './FormAutocomplete/FormAutocomplete'
5
+ import FormSelect from './FormSelect'
6
+
7
+ const FormCatalogType = ({ type, options, autocomplete, ...props }) => {
8
+ if (autocomplete === true) {
9
+ return (
10
+ <FormAutocomplete
11
+ type="autocomplete"
12
+ // eslint-disable-next-line react/jsx-props-no-spreading
13
+ {...props}
14
+ options={options.map(entry => ({ entry: entry.value }))}
15
+ />
16
+ )
17
+ }
18
+
19
+ // eslint-disable-next-line react/jsx-props-no-spreading
20
+ return <FormSelect type={type} options={options} {...props} />
21
+ }
22
+
23
+ FormCatalogType.propTypes = {
24
+ type: PropTypes.string.isRequired,
25
+ options: PropTypes.array.isRequired,
26
+ autocomplete: PropTypes.bool
27
+ }
28
+
29
+ export default FormCatalogType
@@ -27,11 +27,11 @@ const FormControlField = ({
27
27
  componentProps,
28
28
  defaultValue,
29
29
  value,
30
- disabled,
31
30
  options,
32
31
  hasErrors,
33
32
  errors,
34
33
  required,
34
+ disabled,
35
35
  validating,
36
36
  dirty,
37
37
  onChange
@@ -63,11 +63,13 @@ const FormControlField = ({
63
63
  let control
64
64
  if (isRender) {
65
65
  try {
66
+ const oldProps = component.props || {}
66
67
  control = React.cloneElement(
67
68
  component,
68
69
  {
69
- variant,
70
70
  ...finalProps,
71
+ ...oldProps,
72
+ variant,
71
73
  label,
72
74
  hasErrors,
73
75
  errors,
@@ -143,7 +145,6 @@ FormControlField.propTypes = {
143
145
  PropTypes.string,
144
146
  PropTypes.bool
145
147
  ]),
146
- labelPlacement: PropTypes.oneOf(['top', 'start', 'bottom', 'end']),
147
148
  defaultValue: PropTypes.oneOfType([
148
149
  PropTypes.string,
149
150
  PropTypes.number,
@@ -167,6 +168,7 @@ FormControlField.propTypes = {
167
168
  validating: PropTypes.bool,
168
169
  dirty: PropTypes.bool,
169
170
  required: PropTypes.bool,
171
+ disabled: PropTypes.bool,
170
172
  onChange: PropTypes.func.isRequired
171
173
  }
172
174
 
@@ -52,7 +52,8 @@ const FormRepeater = ({ variant, name, value, catalog, onChange }) => {
52
52
  }
53
53
 
54
54
  useEffect(() => {
55
- onChange({ target: { name, value: entries } })
55
+ const newValue = entries.length > 0 ? entries : undefined
56
+ onChange({ target: { name, value: newValue } })
56
57
  }, [entries])
57
58
 
58
59
  let blockedOptions = []
@@ -20,6 +20,7 @@ const FormSelect = ({ options, required, value, ...props }) => {
20
20
  select
21
21
  required={required}
22
22
  value={value === undefined ? '' : value}
23
+ // eslint-disable-next-line react/jsx-props-no-spreading
23
24
  {...props}
24
25
  >
25
26
  {
@@ -1,13 +1,20 @@
1
1
  import React from 'react'
2
+ import PropTypes from 'prop-types'
2
3
 
3
4
  import { TextField } from '@material-ui/core'
4
5
 
5
- // eslint-disable-next-line react/prop-types, react/jsx-props-no-spreading
6
- export default props => {
6
+ const FormText = props => {
7
+ const { type } = props
7
8
  const finalProps = { ...props }
8
- if (props.type === 'date' || props.type === 'datetime-local') {
9
- finalProps.InputLabelProps={ shrink: true }
9
+
10
+ if (type === 'date' || type === 'datetime-local') {
11
+ finalProps.InputLabelProps = { shrink: true }
10
12
  }
11
13
 
12
- return <TextField {...finalProps} />
14
+ // eslint-disable-next-line react/jsx-props-no-spreading
15
+ return <TextField type={type} {...finalProps} />
13
16
  }
17
+
18
+ FormText.propTypes = { type: PropTypes.string }
19
+
20
+ export default FormText
@@ -8,7 +8,7 @@ import isEqual from 'fast-deep-equal'
8
8
  import { render as mm } from 'micromustache'
9
9
 
10
10
  import FormulatorFormSection from './FormulatorFormSection'
11
- import HiddenData from './HiddenData'
11
+ import HiddenData from './Components/HiddenData'
12
12
 
13
13
  import 'highlight.js/styles/shades-of-purple.css'
14
14
 
@@ -410,7 +410,7 @@ class FormulatorForm extends React.Component {
410
410
  renderSchema.groups
411
411
  .map(({ id: sectionId, label, description, fields }) => {
412
412
  if (readOnly && Array.isArray(obscuredGroups) && obscuredGroups.includes(sectionId)) {
413
- return <HiddenData subject="Der Bereich" label={label} />
413
+ return <HiddenData key={`hidden-field-${sectionId}`} subject="Der Bereich" label={label} />
414
414
  }
415
415
 
416
416
  return (
@@ -11,7 +11,7 @@ import FormSectionBlock from './FormSectionBlock'
11
11
  import FormField from './Editable/FormField'
12
12
  import FormText from './Editable/FormText'
13
13
  import FormNumber from './Editable/FormNumber'
14
- import FormSelect from './Editable/FormSelect'
14
+ import FormCatalogType from './Editable/FormCatalogType'
15
15
  import FormBoolean from './Editable/FormBoolean'
16
16
  import FormRepeater from './Editable/FormRepeater'
17
17
 
@@ -23,7 +23,7 @@ import FormReadOnlySelect from './ReadOnly/FormReadOnlySelect'
23
23
  import FormReadOnlyBoolean from './ReadOnly/FormReadOnlyBoolean'
24
24
  import FormReadOnlyRepeater from './ReadOnly/FormReadOnlyRepeater'
25
25
 
26
- import HiddenData from './HiddenData'
26
+ import HiddenData from './Components/HiddenData'
27
27
 
28
28
  const formReadOnlyComponentMap = {
29
29
  default: FormReadOnlyText,
@@ -39,7 +39,7 @@ const formComponentMap = {
39
39
  default: FormText,
40
40
  string: FormText,
41
41
  number: FormNumber,
42
- select: FormSelect,
42
+ select: FormCatalogType,
43
43
  boolean: FormBoolean,
44
44
  array: FormRepeater
45
45
  }
@@ -48,7 +48,7 @@ const apComponentMap = {
48
48
  default: Input,
49
49
  string: Input,
50
50
  number: FormNumber,
51
- select: FormSelect,
51
+ select: FormCatalogType,
52
52
  boolean: FormBoolean
53
53
  }
54
54
 
@@ -242,7 +242,7 @@ class FormulatorFormSection extends React.Component {
242
242
  if (readOnly) {
243
243
  if (!value) return false
244
244
  if (Array.isArray(obscuredFields) && obscuredFields.includes(name)) {
245
- return <HiddenData subject="Das Feld" label={label} />
245
+ return <HiddenData key={`hidden-field-${name}`} subject="Das Feld" label={label} />
246
246
  }
247
247
  }
248
248
 
@@ -28,7 +28,7 @@ const FormControlField = ({
28
28
  }
29
29
 
30
30
  let control
31
- if (type === '@@render') control = React.cloneElement(component, finalProps);
31
+ if (type === '@@render') control = React.cloneElement(component, finalProps)
32
32
  else control = React.createElement(component, finalProps)
33
33
 
34
34
  return (
@@ -5,11 +5,18 @@ import { Typography } from '@material-ui/core'
5
5
 
6
6
  import { DateTime } from 'luxon'
7
7
 
8
- const FormReadOnlyText = ({ value, type }) => {
8
+ const FormReadOnlyText = ({ value, type, renderFormat }) => {
9
9
  let finalValue = value
10
10
 
11
11
  if (type === 'date' || type === 'datetime-local') {
12
- finalValue = DateTime.fromISO(value).toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY)
12
+ let formatter = DateTime.DATETIME_FULL
13
+
14
+ if (renderFormat) {
15
+ if (typeof renderFormat === 'string') formatter = DateTime[renderFormat]
16
+ else formatter = renderFormat
17
+ }
18
+
19
+ finalValue = DateTime.fromISO(value).toLocaleString(formatter)
13
20
  }
14
21
 
15
22
  return (
@@ -23,6 +30,11 @@ FormReadOnlyText.propTypes = {
23
30
  value: PropTypes.oneOfType([
24
31
  PropTypes.number,
25
32
  PropTypes.string
33
+ ]),
34
+ type: PropTypes.string,
35
+ renderFormat: PropTypes.oneOfType([
36
+ PropTypes.string,
37
+ PropTypes.object
26
38
  ])
27
39
  }
28
40
 
package/src/index.js CHANGED
@@ -6,6 +6,7 @@ export FormNumber from './Editable/FormNumber'
6
6
  export FormRepeater from './Editable/FormRepeater'
7
7
  export FormSelect from './Editable/FormSelect'
8
8
  export FormText from './Editable/FormText'
9
+ export FormAutocomplete from './Editable/FormAutocomplete/FormAutocomplete'
9
10
 
10
11
  export FormReadOnlyField from './ReadOnly/FormReadOnlyField'
11
12
  export FormReadOnlyBoolean from './ReadOnly/FormReadOnlyBoolean'
@@ -14,3 +15,6 @@ export FormReadOnlyRepeater from './ReadOnly/FormReadOnlyRepeater'
14
15
  export FormReadOnlySelect from './ReadOnly/FormReadOnlySelect'
15
16
  export FormReadOnlyText from './ReadOnly/FormReadOnlyText'
16
17
  export FormReadOnlyMarkdown from './ReadOnly/FormReadOnlyMarkdown'
18
+
19
+ export HiddenData from './Components/HiddenData'
20
+ export SelectOrCreate from './Components/SelectOrCreate'
@@ -12,7 +12,7 @@ const CustomRenderField = ({ name, value, onChange, ...props }) => {
12
12
  }
13
13
 
14
14
  useEffect(() => {
15
- setDeg(value?.length * 10)
15
+ setDeg((value?.length || 0) * 10)
16
16
  }, [value])
17
17
 
18
18
  return (
@@ -38,9 +38,9 @@ const CustomRenderField = ({ name, value, onChange, ...props }) => {
38
38
  }
39
39
 
40
40
  CustomRenderField.propTypes = {
41
- name: PropTypes.string.isRequired,
41
+ name: PropTypes.string,
42
42
  value: PropTypes.string,
43
- onChange: PropTypes.func.isRequired
43
+ onChange: PropTypes.func
44
44
  }
45
45
 
46
46
  export default CustomRenderField