@startlibs/form 1.0.11 → 1.1.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.
- package/lib/AutoComplete.mjs +550 -0
- package/lib/AutoComplete.mjs.map +1 -0
- package/lib/Checkbox.mjs +76 -0
- package/lib/Checkbox.mjs.map +1 -0
- package/lib/Combobox.mjs +544 -0
- package/lib/Combobox.mjs.map +1 -0
- package/lib/ComboboxWithCustom.mjs +77 -0
- package/lib/ComboboxWithCustom.mjs.map +1 -0
- package/lib/DatePicker.mjs +625 -0
- package/lib/DatePicker.mjs.map +1 -0
- package/lib/EditableBox.mjs +496 -0
- package/lib/EditableBox.mjs.map +1 -0
- package/lib/Errors.mjs +176 -0
- package/lib/Errors.mjs.map +1 -0
- package/lib/ExpandableBox.mjs +209 -0
- package/lib/ExpandableBox.mjs.map +1 -0
- package/lib/Field.mjs +65 -0
- package/lib/Field.mjs.map +1 -0
- package/lib/FieldBox.mjs +80 -0
- package/lib/FieldBox.mjs.map +1 -0
- package/lib/Form.mjs +322 -0
- package/lib/Form.mjs.map +1 -0
- package/lib/FormValue.mjs +111 -0
- package/lib/FormValue.mjs.map +1 -0
- package/lib/InputboxGroup.mjs +104 -0
- package/lib/InputboxGroup.mjs.map +1 -0
- package/lib/Radiobox.mjs +67 -0
- package/lib/Radiobox.mjs.map +1 -0
- package/lib/RichText.mjs +634 -0
- package/lib/RichText.mjs.map +1 -0
- package/lib/TextInput.mjs +359 -0
- package/lib/TextInput.mjs.map +1 -0
- package/lib/alt/checkbox/SimpleCheckbox.mjs +82 -0
- package/lib/alt/checkbox/SimpleCheckbox.mjs.map +1 -0
- package/lib/alt/checkbox/ToggleCheckbox.mjs +68 -0
- package/lib/alt/checkbox/ToggleCheckbox.mjs.map +1 -0
- package/lib/alt/radiobox/CleanTabRadiobox.mjs +44 -0
- package/lib/alt/radiobox/CleanTabRadiobox.mjs.map +1 -0
- package/lib/alt/radiobox/IconRadioBox.mjs +52 -0
- package/lib/alt/radiobox/IconRadioBox.mjs.map +1 -0
- package/lib/alt/radiobox/SimpleRadiobox.mjs +61 -0
- package/lib/alt/radiobox/SimpleRadiobox.mjs.map +1 -0
- package/lib/alt/radiobox/TabRadiobox.mjs +54 -0
- package/lib/alt/radiobox/TabRadiobox.mjs.map +1 -0
- package/lib/enhanceInput.mjs +131 -0
- package/lib/enhanceInput.mjs.map +1 -0
- package/lib/index.cjs +5345 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.mjs +28 -0
- package/lib/index.mjs.map +1 -0
- package/lib/useConfirmDialog.mjs +117 -0
- package/lib/useConfirmDialog.mjs.map +1 -0
- package/lib/useDraftConfirmation.mjs +56 -0
- package/lib/useDraftConfirmation.mjs.map +1 -0
- package/lib/validation.mjs +91 -0
- package/lib/validation.mjs.map +1 -0
- package/lib/withFormImageInput.mjs +281 -0
- package/lib/withFormImageInput.mjs.map +1 -0
- package/package.json +44 -25
- package/src/AutoComplete.jsx +642 -0
- package/src/Checkbox.jsx +45 -0
- package/src/Combobox.jsx +606 -0
- package/src/ComboboxWithCustom.jsx +68 -0
- package/src/DatePicker.jsx +733 -0
- package/src/EditableBox.jsx +502 -0
- package/src/Errors.jsx +174 -0
- package/src/ExpandableBox.jsx +207 -0
- package/src/Field.jsx +76 -0
- package/src/FieldBox.jsx +136 -0
- package/src/Form.jsx +291 -0
- package/src/FormValue.jsx +67 -0
- package/src/IconRadioBoxOldHubt.jsx +93 -0
- package/src/InputboxGroup.jsx +121 -0
- package/src/Radiobox.jsx +33 -0
- package/src/RichText.jsx +812 -0
- package/src/TextInput.jsx +519 -0
- package/src/alt/checkbox/SimpleCheckbox.jsx +162 -0
- package/src/alt/checkbox/ToggleCheckbox.jsx +143 -0
- package/src/alt/radiobox/CleanTabRadiobox.jsx +46 -0
- package/src/alt/radiobox/IconRadioBox.jsx +93 -0
- package/src/alt/radiobox/SimpleRadiobox.jsx +153 -0
- package/src/alt/radiobox/TabRadiobox.jsx +73 -0
- package/src/enhanceInput.jsx +107 -0
- package/src/index.js +27 -0
- package/src/useConfirmDialog.jsx +105 -0
- package/src/useDraftConfirmation.jsx +60 -0
- package/src/validation.js +92 -0
- package/src/withFormImageInput.jsx +297 -0
- package/.babelrc +0 -28
- package/lib/index.js +0 -6255
- package/rollup.config.js +0 -43
- package/yarn-error.log +0 -1607
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef} from 'react'
|
|
2
|
+
import _ from 'lodash/fp'
|
|
3
|
+
import {
|
|
4
|
+
StartlibsConsumer,
|
|
5
|
+
useConsumerCatalog,
|
|
6
|
+
useLazyConstant,
|
|
7
|
+
usePopupToggle,
|
|
8
|
+
useRefState,
|
|
9
|
+
useToggle,
|
|
10
|
+
useRefMemo
|
|
11
|
+
} from '@startlibs/core'
|
|
12
|
+
import {Button, ContextMenu, fixDoubledLastMarginBottom, Li, willUseFlipSwitch} from '@startlibs/components'
|
|
13
|
+
import {Errors} from './Errors'
|
|
14
|
+
import {FormContext} from './Form'
|
|
15
|
+
import {FormValue} from './FormValue'
|
|
16
|
+
import {PathContext} from './enhanceInput'
|
|
17
|
+
import {useDraftConfirmation} from './useDraftConfirmation'
|
|
18
|
+
import styled from 'styled-components'
|
|
19
|
+
import {_s, callIfFunction, formatPath, lazyValue, stopPropagation} from '@startlibs/utils'
|
|
20
|
+
|
|
21
|
+
const addRootPath = (errorRootPath, errors, path) => {
|
|
22
|
+
const rootPath = [].concat(errorRootPath || [], path)
|
|
23
|
+
return Object.keys(errors).reduce((acc, error) => {
|
|
24
|
+
acc[rootPath.concat(error).join('.')] = errors[error]
|
|
25
|
+
return acc
|
|
26
|
+
}, {})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const EditableContext = React.createContext({})
|
|
30
|
+
|
|
31
|
+
export const ConfirmEditableBoxes = forwardRef(({children, itemValidation = _.stubFalse, listValidation = () => false, transform = _.identity}, ref) => {
|
|
32
|
+
|
|
33
|
+
const submitAll = useCallback(async (withLoading) => {
|
|
34
|
+
if (withLoading) {
|
|
35
|
+
editableBoxes().forEach(_ => _.loading().open())
|
|
36
|
+
}
|
|
37
|
+
return Promise
|
|
38
|
+
.all([submitAllShared(), submitAllStandalone()])
|
|
39
|
+
.then(([shared, standalone]) => {
|
|
40
|
+
if (withLoading) {
|
|
41
|
+
editableBoxes().forEach(_ => _.loading().close())
|
|
42
|
+
}
|
|
43
|
+
return shared ? (standalone ? {shared, standalone} : shared) : standalone
|
|
44
|
+
})
|
|
45
|
+
.catch(() => withLoading && editableBoxes().forEach(_ => _.loading().close()))
|
|
46
|
+
}, []
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const [confirmDraftContent, draftToggle, draftConfirmation] = useDraftConfirmation(submitAll)
|
|
50
|
+
|
|
51
|
+
useEffect(()=> draftToggle.openWith(lazyValue(() => hasModifiedBoxes())),[])
|
|
52
|
+
|
|
53
|
+
const [sharedBoxes, sharedBoxesManager] = useConsumerCatalog()
|
|
54
|
+
const [standaloneBoxes, standaloneBoxesManager] = useConsumerCatalog()
|
|
55
|
+
|
|
56
|
+
const editableBoxes = (boxes) => boxes || sharedBoxes().concat(standaloneBoxes())
|
|
57
|
+
const hasEditableBoxes = (boxes) => editableBoxes(boxes).length > 0
|
|
58
|
+
|
|
59
|
+
const provide = useMemo(() => ({
|
|
60
|
+
sharedBoxes: sharedBoxesManager,
|
|
61
|
+
standaloneBoxes: standaloneBoxesManager,
|
|
62
|
+
itemValidation,
|
|
63
|
+
transform
|
|
64
|
+
}), [])
|
|
65
|
+
|
|
66
|
+
const confirm = useCallback((cb, dialogType = 'submit-draft') => draftConfirmation(dialogType, cb), [draftConfirmation])
|
|
67
|
+
const discard = useCallback((cb) => draftConfirmation('discard-draft', cb), [draftConfirmation])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
const modifiedEditablesPath = (boxes) => modifiedEditableBoxes(boxes).map(_ => callIfFunction(_.path))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
const modifiedEditableBoxes = (boxes) => editableBoxes(boxes).filter(_ => _.modifiedValue()[1])
|
|
74
|
+
const hasModifiedBoxes = (boxes) => modifiedEditableBoxes(boxes).length > 0
|
|
75
|
+
|
|
76
|
+
const submitAllShared = () => {
|
|
77
|
+
if (!sharedBoxes().length) return false
|
|
78
|
+
const sharedWithForm = sharedBoxes().find(_ => _.form)
|
|
79
|
+
if (!sharedWithForm) {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
const {form} = sharedWithForm.props
|
|
83
|
+
const validated = modifiedEditableBoxes(sharedBoxes()).reduce(({success, errors}, box) => {
|
|
84
|
+
const index = callIfFunction(box.path)
|
|
85
|
+
const [newItem, wasModified] = box.modifiedValue()
|
|
86
|
+
const newErrors = wasModified ? itemValidation(newItem, index) : false
|
|
87
|
+
if (newErrors) {
|
|
88
|
+
return {
|
|
89
|
+
success,
|
|
90
|
+
errors: Object.assign({}, errors, addRootPath(form.meta.errorRootPath, newErrors, index))
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const newProps = _s.set(index, transform(newItem, index), success)
|
|
94
|
+
return {success: newProps, errors}
|
|
95
|
+
}, {success: form.getOriginalValues(), errors: undefined})
|
|
96
|
+
if (validated.errors) {
|
|
97
|
+
form.setErrors(validated.errors)
|
|
98
|
+
return Promise.resolve(validated)
|
|
99
|
+
}
|
|
100
|
+
return form.submitForm(validated.success)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const submitAllStandalone = () => {
|
|
104
|
+
if (!standaloneBoxes().length) return false
|
|
105
|
+
const edited = standaloneBoxes()
|
|
106
|
+
.filter(_ => _.form.hasChanged)
|
|
107
|
+
const notValidated = edited.filter(_ => !_.form.validate())
|
|
108
|
+
if (notValidated.length) {
|
|
109
|
+
return Promise.reject({errors: notValidated})
|
|
110
|
+
} else {
|
|
111
|
+
return Promise
|
|
112
|
+
.all(edited.map(_ => _.handleSubmit()))
|
|
113
|
+
.then(boxes => {
|
|
114
|
+
const boxesAndResult = boxes
|
|
115
|
+
.map((_, i) => [edited[i], _, i])
|
|
116
|
+
boxesAndResult.filter(([edited, result, i]) => !result.errors).forEach(([_]) => _.close())
|
|
117
|
+
const failed = boxesAndResult.filter(([edited, result, i]) => result.errors)
|
|
118
|
+
if (failed.length) {
|
|
119
|
+
return {errors: failed}
|
|
120
|
+
} else {
|
|
121
|
+
return {success: boxes}
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const methods = useMemo(() => ({
|
|
128
|
+
confirm, discard, submitAll, hasEditableBoxes, editableBoxes, modifiedEditablesPath
|
|
129
|
+
}), [draftConfirmation])
|
|
130
|
+
|
|
131
|
+
useImperativeHandle(ref, () => methods, [methods])
|
|
132
|
+
|
|
133
|
+
return <>
|
|
134
|
+
{confirmDraftContent}
|
|
135
|
+
<EditableContext.Provider value={provide}>
|
|
136
|
+
{callIfFunction(children, methods)}
|
|
137
|
+
</EditableContext.Provider>
|
|
138
|
+
</>
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const FlipDiv = ({useAnimation, ...props}) => {
|
|
142
|
+
const ref = useRef()
|
|
143
|
+
useAnimation(ref)
|
|
144
|
+
return <div {...props} ref={ref}/>
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const EditableBoxPresentation = styled(forwardRef((
|
|
148
|
+
{
|
|
149
|
+
view = (value) => value === undefined || value === null ? '' : String(value),
|
|
150
|
+
tabIndex = 1,
|
|
151
|
+
onClose,
|
|
152
|
+
editingIsOpen,
|
|
153
|
+
onOpen,
|
|
154
|
+
onDelete,
|
|
155
|
+
onCancel,
|
|
156
|
+
onSave,
|
|
157
|
+
onMouseDown,
|
|
158
|
+
onTouchStart,
|
|
159
|
+
resetField,
|
|
160
|
+
children,
|
|
161
|
+
withDelete,
|
|
162
|
+
withContextMenu,
|
|
163
|
+
edit,
|
|
164
|
+
readOnly,
|
|
165
|
+
disabled,
|
|
166
|
+
isNew,
|
|
167
|
+
saveLabel,
|
|
168
|
+
className,
|
|
169
|
+
submitRef,
|
|
170
|
+
path
|
|
171
|
+
},
|
|
172
|
+
ref
|
|
173
|
+
) => {
|
|
174
|
+
const popup = usePopupToggle()
|
|
175
|
+
const editing = useToggle(editingIsOpen)
|
|
176
|
+
const loading = useToggle()
|
|
177
|
+
|
|
178
|
+
const form = useContext(FormContext)
|
|
179
|
+
|
|
180
|
+
const closeBox = (params) => {
|
|
181
|
+
callIfFunction(onClose,params)
|
|
182
|
+
editing.close()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
return () => callIfFunction(onClose, true)
|
|
187
|
+
}, [])
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (editing.isOpen) {
|
|
191
|
+
callIfFunction(onOpen)
|
|
192
|
+
}
|
|
193
|
+
}, [editing.isOpen])
|
|
194
|
+
|
|
195
|
+
useImperativeHandle(ref, () => ({
|
|
196
|
+
loading,
|
|
197
|
+
closeBox,
|
|
198
|
+
handleCancel,
|
|
199
|
+
getValue
|
|
200
|
+
}))
|
|
201
|
+
|
|
202
|
+
const handleDelete = (e) => {
|
|
203
|
+
if (e) e.preventDefault()
|
|
204
|
+
onDelete()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const tryClose = (e) => {
|
|
208
|
+
if (e.key === 'Escape' && editing.isOpen) {
|
|
209
|
+
e.preventDefault()
|
|
210
|
+
e.stopPropagation()
|
|
211
|
+
handleCancel()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const handleCancel = () => {
|
|
216
|
+
onCancel(closeBox)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const onSubmit = (e) => {
|
|
220
|
+
if (e) e.preventDefault()
|
|
221
|
+
loading.wait(() => onSave().then(() => closeBox(true)).catch(()=>{}))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const getPath = (extraPath) => _s.toPath(path).concat(_s.toPath(extraPath))
|
|
225
|
+
|
|
226
|
+
const getValue = (extraPath, otherProps) => otherProps ? _s.get(getPath(extraPath), otherProps) : form.getValue(getPath(extraPath))
|
|
227
|
+
const setValue = (value, extraPath) => form.setValue(getPath(extraPath),value)
|
|
228
|
+
|
|
229
|
+
const flipAnimation = useLazyConstant(willUseFlipSwitch)
|
|
230
|
+
|
|
231
|
+
const disabledClick = readOnly || disabled
|
|
232
|
+
|
|
233
|
+
const openBox = () => {
|
|
234
|
+
editing.open()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const formattedPath = getPath()
|
|
238
|
+
return (
|
|
239
|
+
<PathContext.Provider value={formattedPath}>
|
|
240
|
+
<FlipDiv
|
|
241
|
+
key={editing.isOpen}
|
|
242
|
+
useAnimation={flipAnimation}
|
|
243
|
+
onKeyDown={tryClose}
|
|
244
|
+
onMouseDown={onMouseDown}
|
|
245
|
+
onTouchStart={onTouchStart}
|
|
246
|
+
className={'editable-box ' + (!disabledClick ? 'clickable ' : '') + (editing.isOpen ? 'expanded' : 'collapsed') + ' ' + className}
|
|
247
|
+
onClick={editing.isOpen || disabledClick ? undefined : openBox}
|
|
248
|
+
>
|
|
249
|
+
{
|
|
250
|
+
!editing.isOpen && !disabledClick && !(withDelete || withContextMenu) &&
|
|
251
|
+
<Button tabIndex={tabIndex} className="edit-button" small onlyIcon icon="edit"/>
|
|
252
|
+
}
|
|
253
|
+
{
|
|
254
|
+
!editing.isOpen && !disabledClick && (withDelete || withContextMenu) &&
|
|
255
|
+
<Button tabIndex={tabIndex} className="edit-button arrow-down" small onlyIcon icon="arrow-down"
|
|
256
|
+
onClick={stopPropagation(popup.toggle)}>
|
|
257
|
+
{
|
|
258
|
+
popup.isOpen &&
|
|
259
|
+
<ContextMenu>
|
|
260
|
+
<Li label="Editar" icon="edit-line" onClick={openBox}/>
|
|
261
|
+
{withContextMenu}
|
|
262
|
+
{withDelete && <Li label="Deletar" icon="remove" onClick={handleDelete}/>}
|
|
263
|
+
</ContextMenu>
|
|
264
|
+
}
|
|
265
|
+
</Button>
|
|
266
|
+
}
|
|
267
|
+
<form onSubmit={onSubmit}>
|
|
268
|
+
<FormValue path={formattedPath} key={formatPath(formattedPath)}>{(value) =>
|
|
269
|
+
fixDoubledLastMarginBottom(
|
|
270
|
+
editing.isOpen
|
|
271
|
+
? edit(value, { getPath, getValue, path: formattedPath, tabIndex, setValue})
|
|
272
|
+
: view(value, { getPath, getValue, path: formattedPath })
|
|
273
|
+
)
|
|
274
|
+
}</FormValue>
|
|
275
|
+
<Errors path={formattedPath}/>
|
|
276
|
+
{
|
|
277
|
+
editing.isOpen &&
|
|
278
|
+
<div className="editable-box-footer">
|
|
279
|
+
<div>
|
|
280
|
+
{
|
|
281
|
+
withDelete &&
|
|
282
|
+
<Button
|
|
283
|
+
icon="remove"
|
|
284
|
+
hover="alert"
|
|
285
|
+
tabIndex={tabIndex + 2}
|
|
286
|
+
onClick={handleDelete}
|
|
287
|
+
>
|
|
288
|
+
Deletar
|
|
289
|
+
</Button>
|
|
290
|
+
}
|
|
291
|
+
</div>
|
|
292
|
+
<StartlibsConsumer>{({labels}) =>
|
|
293
|
+
<div>
|
|
294
|
+
<Button
|
|
295
|
+
tabIndex={tabIndex + 1}
|
|
296
|
+
onClick={handleCancel}
|
|
297
|
+
>
|
|
298
|
+
{labels.EditableBox.cancel}
|
|
299
|
+
</Button>
|
|
300
|
+
<Button
|
|
301
|
+
buttonRef={submitRef}
|
|
302
|
+
isLoading={loading.isOpen}
|
|
303
|
+
highlight
|
|
304
|
+
tabIndex={tabIndex}
|
|
305
|
+
type="submit"
|
|
306
|
+
>
|
|
307
|
+
{callIfFunction(saveLabel,isNew) || (isNew ? labels.EditableBox.saveNew : labels.EditableBox.save)}
|
|
308
|
+
</Button>
|
|
309
|
+
</div>
|
|
310
|
+
}
|
|
311
|
+
</StartlibsConsumer>
|
|
312
|
+
</div>
|
|
313
|
+
}
|
|
314
|
+
</form>
|
|
315
|
+
{children}
|
|
316
|
+
</FlipDiv>
|
|
317
|
+
</PathContext.Provider>
|
|
318
|
+
)
|
|
319
|
+
}))`
|
|
320
|
+
${props => props.disabled && "opacity: 0.7;"}
|
|
321
|
+
`
|
|
322
|
+
|
|
323
|
+
const SharedEditableTabBox = ({path,isSingle, checkIsNew = (a,b) => a!==b, ...props}) => {
|
|
324
|
+
|
|
325
|
+
const editableGroup = useContext(EditableContext)
|
|
326
|
+
const form = useContext(FormContext)
|
|
327
|
+
|
|
328
|
+
const isNew = useRefState(() => isSingle ? false : checkIsNew(_s.get(path, form.getOriginalValues()),form.getValue(path)))
|
|
329
|
+
const internalBox = React.useRef()
|
|
330
|
+
const pathRef = useRefMemo(path)
|
|
331
|
+
|
|
332
|
+
const loading = () => internalBox.current.loading
|
|
333
|
+
const modifiedValue = () => {
|
|
334
|
+
const oldItem = internalBox.current.getValue(undefined, form.getOriginalValues())
|
|
335
|
+
const newItem = internalBox.current.getValue()
|
|
336
|
+
return newItem !== oldItem ? [newItem, true] : [oldItem, false]
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const selfRef = useRef({
|
|
340
|
+
loading,path:() => pathRef.current,form,modifiedValue
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
const resetField = () => form.resetField([].concat(path))
|
|
344
|
+
|
|
345
|
+
const onClose = (skipReset) => {
|
|
346
|
+
if (!skipReset) {
|
|
347
|
+
resetField()
|
|
348
|
+
}
|
|
349
|
+
form.clearErrors(_s.toPath(path))
|
|
350
|
+
editableGroup.sharedBoxes.unregister(selfRef.current)
|
|
351
|
+
isNew.set(false)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const onOpen = () => {
|
|
355
|
+
editableGroup.sharedBoxes.register(selfRef.current)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
const validate = (props, path) => {
|
|
360
|
+
const errors = editableGroup.itemValidation(props, path)
|
|
361
|
+
if (errors) {
|
|
362
|
+
form.setErrors(addRootPath(form.getMeta('errorRootPath'), errors, path))
|
|
363
|
+
}
|
|
364
|
+
return errors
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const onSave = () => {
|
|
368
|
+
const [newItem, wasModified] = modifiedValue()
|
|
369
|
+
if (!wasModified) {
|
|
370
|
+
internalBox.current.closeBox()
|
|
371
|
+
return Promise.resolve({})
|
|
372
|
+
}
|
|
373
|
+
const errors = validate(newItem, path)
|
|
374
|
+
if (errors) {
|
|
375
|
+
return Promise.reject({errors})
|
|
376
|
+
}
|
|
377
|
+
const transform = editableGroup.transform
|
|
378
|
+
if (isNew.get()) {
|
|
379
|
+
const arrayPath = _s.toPath(path)
|
|
380
|
+
const index = arrayPath.slice(-1)
|
|
381
|
+
const newProps = _s.update(arrayPath.slice(0,-1),_s.insertAt(index, transform(newItem, path)), form.getOriginalValues())
|
|
382
|
+
return form.submitForm(newProps)
|
|
383
|
+
} else {
|
|
384
|
+
const newProps = _s.set(path, transform(newItem, path), form.getOriginalValues())
|
|
385
|
+
return form.submitForm(newProps)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const onDelete = () => {
|
|
390
|
+
if (isNew.get()) {
|
|
391
|
+
onCancel()
|
|
392
|
+
return Promise.resolve({success: true})
|
|
393
|
+
}
|
|
394
|
+
const original = form.getOriginalValues()
|
|
395
|
+
const newProps = _s.unsetOrRemoveAt(path, original)
|
|
396
|
+
if (_s.get(_s.toPath(path).slice(0, -1), original).length !== _s.get(_s.toPath(path).slice(0, -1), newProps).length) {
|
|
397
|
+
internalBox.current.loading.open()
|
|
398
|
+
const fetch = form.submitForm(newProps)
|
|
399
|
+
fetch.then(({errors}) => {
|
|
400
|
+
internalBox.current.loading.close()
|
|
401
|
+
if (!errors) {
|
|
402
|
+
form.setValues(_s.unsetOrRemoveAt(path))
|
|
403
|
+
}
|
|
404
|
+
}).catch(() => internalBox.current.loading.close())
|
|
405
|
+
return fetch
|
|
406
|
+
} else {
|
|
407
|
+
internalBox.current.handleCancel()
|
|
408
|
+
return Promise.resolve({success: true})
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const onCancel = (close) => {
|
|
413
|
+
const lastKey = Array.isArray(path) ? path.slice(-1)[0] : path
|
|
414
|
+
const prevKeys = Array.isArray(path) ? path.slice(0, path.length - 1) : []
|
|
415
|
+
if (isNew.get() && typeof lastKey === 'number') {
|
|
416
|
+
form.setValues(_s.unsetOrRemoveAt(path))
|
|
417
|
+
} else {
|
|
418
|
+
form.resetField(path)
|
|
419
|
+
close()
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return <EditableBoxPresentation
|
|
424
|
+
ref={internalBox}
|
|
425
|
+
resetField={resetField}
|
|
426
|
+
onOpen={onOpen}
|
|
427
|
+
onClose={onClose}
|
|
428
|
+
onDelete={onDelete}
|
|
429
|
+
onCancel={onCancel}
|
|
430
|
+
onSave={onSave}
|
|
431
|
+
form={editableGroup.form}
|
|
432
|
+
isNew={isNew.get()}
|
|
433
|
+
path={path}
|
|
434
|
+
{...props}
|
|
435
|
+
/>
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export const EditableBox = ({path, onCancel, onClose, isNew,...props}) => {
|
|
439
|
+
|
|
440
|
+
const editableGroup = useContext(EditableContext)
|
|
441
|
+
const form = useContext(FormContext)
|
|
442
|
+
|
|
443
|
+
const internalBox = React.useRef()
|
|
444
|
+
|
|
445
|
+
const resetField = () => path ? form.resetField(_s.toPath(path)) : form.resetForm()
|
|
446
|
+
|
|
447
|
+
const _onClose = (skipReset) => {
|
|
448
|
+
if (!skipReset) {
|
|
449
|
+
resetField()
|
|
450
|
+
}
|
|
451
|
+
form.clearErrors(path)
|
|
452
|
+
editableGroup.standaloneBoxes.unregister(selfRef.current)
|
|
453
|
+
callIfFunction(onClose)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const modifiedValue = () => {
|
|
457
|
+
const oldItem = internalBox.current.getValue(undefined, form.getOriginalValues())
|
|
458
|
+
const newItem = internalBox.current.getValue()
|
|
459
|
+
return newItem !== oldItem ? [newItem, true] : [oldItem, false]
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const onOpen = () => {
|
|
463
|
+
editableGroup.standaloneBoxes.register(selfRef.current)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const onDelete = () => {
|
|
467
|
+
props.removeItem(props.menuId)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const _onCancel = (close) => {
|
|
471
|
+
if (callIfFunction(onCancel,close) !== false) {
|
|
472
|
+
close()
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const close = () => internalBox.current.closeBox()
|
|
477
|
+
|
|
478
|
+
const onSave = () => form.submitForm()
|
|
479
|
+
|
|
480
|
+
const handleSubmit = () => form.handleSubmit()
|
|
481
|
+
|
|
482
|
+
const loading = () => internalBox.current.loading
|
|
483
|
+
|
|
484
|
+
const pathRef = useRefMemo(path)
|
|
485
|
+
const selfRef = useRef({loading,path:() => pathRef.current,form,modifiedValue,handleSubmit,close})
|
|
486
|
+
|
|
487
|
+
return <EditableBoxPresentation
|
|
488
|
+
ref={internalBox}
|
|
489
|
+
resetField={resetField}
|
|
490
|
+
onOpen={onOpen}
|
|
491
|
+
onClose={_onClose}
|
|
492
|
+
onDelete={onDelete}
|
|
493
|
+
onCancel={_onCancel}
|
|
494
|
+
onSave={onSave}
|
|
495
|
+
form={form}
|
|
496
|
+
path={path}
|
|
497
|
+
isNew={isNew}
|
|
498
|
+
{...props}
|
|
499
|
+
/>
|
|
500
|
+
}
|
|
501
|
+
EditableBox.Shared = SharedEditableTabBox
|
|
502
|
+
|
package/src/Errors.jsx
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React, {useEffect, useContext, useRef, useMemo} from 'react'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
import {Icon,TransitionDiv} from '@startlibs/components'
|
|
4
|
+
import {usePreviousForEffect, useToggle} from '@startlibs/core'
|
|
5
|
+
import {lighten} from 'polished'
|
|
6
|
+
import {createSelector} from 'reselect'
|
|
7
|
+
import _ from 'lodash/fp'
|
|
8
|
+
import {formatPath, getColor, customTheme, identityFn, isElementInView} from '@startlibs/utils'
|
|
9
|
+
|
|
10
|
+
const errorsInsidePathSelector = () => createSelector(
|
|
11
|
+
[identityFn(0), identityFn(1)],
|
|
12
|
+
(path, entries) =>
|
|
13
|
+
path ? entries.filter(([_, p]) => (p + '.').indexOf(path + '.') === 0) : entries
|
|
14
|
+
)
|
|
15
|
+
const parseError = (value, path) => Array.isArray(value) ? value.map(v => [v, formatPath(path)]) : [[value, formatPath(path)]]
|
|
16
|
+
|
|
17
|
+
const parseMultipleErrors = (objOrArray) => [].concat.apply([],
|
|
18
|
+
(Array.isArray(objOrArray) ? objOrArray : Object.entries(objOrArray))
|
|
19
|
+
.map(([path, value]) => parseError(value, path))
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
export const ErrorsContext = React.createContext({})
|
|
23
|
+
const ErrorFilterContext = React.createContext(_.identity)
|
|
24
|
+
export const ErrorFilterProvider = ErrorFilterContext.Provider
|
|
25
|
+
|
|
26
|
+
export const useProvideErrors = () => {
|
|
27
|
+
const errors = useToggle([])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const addError = (value, path) => errors.openWith(errors => errors.concat(parseError(value, path)))
|
|
31
|
+
const addErrors = (newErrors) => errors.openWith(errors => errors.concat(parseMultipleErrors(newErrors)))
|
|
32
|
+
|
|
33
|
+
const removeError = (value, path) => errors.openWith(errors => errors.filter(([v, p]) => !(value === v && (p + '.').indexOf(formatPath(path) + '.') >= 0)))
|
|
34
|
+
|
|
35
|
+
const removeErrorPath = (path) => errors.openWith(errors => errors.filter(([_, p]) => formatPath(path) !== p))
|
|
36
|
+
const clearErrors = (path) => errors.openWith(errors => path ? errors.filter(([_, p]) => (p + '.').indexOf(formatPath(path) + '.') < 0) : [])
|
|
37
|
+
|
|
38
|
+
const setErrors = (newErrors) => {
|
|
39
|
+
console.log(parseMultipleErrors(newErrors))
|
|
40
|
+
errors.openWith(parseMultipleErrors(newErrors))
|
|
41
|
+
errors.get()
|
|
42
|
+
}
|
|
43
|
+
const setError = (errorPath, errorValue) => errors.openWith(parseMultipleErrors([[errorPath, errorValue]]))
|
|
44
|
+
|
|
45
|
+
const memoizeErorrsInsidePath = useMemo(errorsInsidePathSelector, [])
|
|
46
|
+
|
|
47
|
+
const getErrorsInsidePath = (path) => memoizeErorrsInsidePath(formatPath(path), errors.get())
|
|
48
|
+
|
|
49
|
+
const utils = {
|
|
50
|
+
addErrors,
|
|
51
|
+
addError,
|
|
52
|
+
setErrors,
|
|
53
|
+
setError,
|
|
54
|
+
clearErrors,
|
|
55
|
+
removeError,
|
|
56
|
+
willRemoveError: (value, insidePath) => () => removeError(value, insidePath),
|
|
57
|
+
removeErrorPath,
|
|
58
|
+
getErrorsInsidePath,
|
|
59
|
+
hasErrors: (path) => getErrorsInsidePath(path).length > 0,
|
|
60
|
+
hasError: (path,value) => getErrorsInsidePath(path).find(([v]) => v === value)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const paths = useMemo(() => new Set(errors.isOpen.map(([_, path]) => path).filter(_ => _)), [errors.isOpen])
|
|
64
|
+
const providerValue = useMemo(() => ({paths, errors:errors.isOpen, ...utils}), [errors.isOpen])
|
|
65
|
+
|
|
66
|
+
return [ErrorsContext.Provider, providerValue, utils]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const useErrors = () => {
|
|
70
|
+
const [Provider,value,utils] = useProvideErrors()
|
|
71
|
+
|
|
72
|
+
return [utils,<Provider value={value}><Errors/></Provider>]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const useHasError = (path) => {
|
|
76
|
+
const {paths} = useContext(ErrorsContext)
|
|
77
|
+
return paths.has(formatPath(path))
|
|
78
|
+
}
|
|
79
|
+
export const useErrorsUtils = () => {
|
|
80
|
+
return useContext(ErrorsContext)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const useTransientErrorHelper = () => {
|
|
84
|
+
const utils = useErrorsUtils()
|
|
85
|
+
const showingErrors = useRef([])
|
|
86
|
+
const clearCurrentErrors = () => {
|
|
87
|
+
showingErrors.current.forEach(([path,value]) => utils?.removeError(value,path))
|
|
88
|
+
showingErrors.current = []
|
|
89
|
+
}
|
|
90
|
+
const setErrors = (newErrors) => {
|
|
91
|
+
clearCurrentErrors()
|
|
92
|
+
utils?.addErrors(newErrors)
|
|
93
|
+
showingErrors.current = newErrors
|
|
94
|
+
}
|
|
95
|
+
useEffect(() => () => {
|
|
96
|
+
clearCurrentErrors()
|
|
97
|
+
},[])
|
|
98
|
+
|
|
99
|
+
return [setErrors,clearCurrentErrors]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const Errors = ({path,filter,className, autoScroll}) => {
|
|
103
|
+
const {getErrorsInsidePath, willRemoveError} = useContext(ErrorsContext)
|
|
104
|
+
const contextFilter = useContext(ErrorFilterContext)
|
|
105
|
+
const ref = useRef()
|
|
106
|
+
|
|
107
|
+
const errors = filter ? getErrorsInsidePath(path).filter(filter) : contextFilter(getErrorsInsidePath(path))
|
|
108
|
+
const uniqErrors = _.uniqBy(_.property(0),errors).filter(([v]) => v)
|
|
109
|
+
const prevErrors = usePreviousForEffect(uniqErrors)
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const changed = _.difference(uniqErrors,prevErrors)
|
|
112
|
+
if (autoScroll && changed.length && ref.current?.scrollIntoView) {
|
|
113
|
+
isElementInView(ref.current).then((inView) => {
|
|
114
|
+
if (!inView) {
|
|
115
|
+
ref.current.scrollIntoView({behavior: "smooth"})
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
},[uniqErrors])
|
|
120
|
+
|
|
121
|
+
return <TransitionDiv block>{uniqErrors.length > 0 && <Errors.Container ref={ref} className={className}>
|
|
122
|
+
{uniqErrors.map(([value]) => value && value !== true &&
|
|
123
|
+
<Errors.Item key={value}>
|
|
124
|
+
{value}
|
|
125
|
+
<Errors.CloseIcon onClick={willRemoveError(value, path)}/>
|
|
126
|
+
</Errors.Item>
|
|
127
|
+
)}
|
|
128
|
+
</Errors.Container>}</TransitionDiv>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
Errors.displayName = 'Errors'
|
|
132
|
+
|
|
133
|
+
Errors.Container = styled.ul`
|
|
134
|
+
list-style-type: none;
|
|
135
|
+
margin: 0;
|
|
136
|
+
padding: 0;
|
|
137
|
+
`
|
|
138
|
+
|
|
139
|
+
Errors.Item = styled.li`
|
|
140
|
+
background-color: ${props => lighten(0.42, props.theme.colors.alert)};
|
|
141
|
+
padding: 1rem 3.5rem 1rem 1rem;
|
|
142
|
+
border-radius: 8px;
|
|
143
|
+
border: 1px solid ${props => lighten(0.35, props.theme.colors.alert)};
|
|
144
|
+
color: ${getColor('alert')};
|
|
145
|
+
position:relative;
|
|
146
|
+
text-align: left;
|
|
147
|
+
font-size: 13px;
|
|
148
|
+
line-height: 130%;
|
|
149
|
+
& ~ & {
|
|
150
|
+
margin-top: 0.5rem;
|
|
151
|
+
}
|
|
152
|
+
${customTheme('ErrorItem')}
|
|
153
|
+
`
|
|
154
|
+
Errors.CloseIcon = styled(Icon).attrs({icon: 'x'})`
|
|
155
|
+
color: ${getColor('alert')};
|
|
156
|
+
font-weight: 600;
|
|
157
|
+
display: inline-block;
|
|
158
|
+
background-color: ${props => lighten(0.35, props.theme.colors.alert)};
|
|
159
|
+
width: 21px;
|
|
160
|
+
height: 21px;
|
|
161
|
+
line-height: 21px;
|
|
162
|
+
text-align: center;
|
|
163
|
+
border-radius: 50%;
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
position: absolute;
|
|
166
|
+
top: 10px;
|
|
167
|
+
right: 10px;
|
|
168
|
+
font-size: 14px;
|
|
169
|
+
z-index: 2;
|
|
170
|
+
&:hover {
|
|
171
|
+
background-color: ${props => lighten(0.32, props.theme.colors.alert)};
|
|
172
|
+
}
|
|
173
|
+
`
|
|
174
|
+
|