@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.
Files changed (92) hide show
  1. package/lib/AutoComplete.mjs +550 -0
  2. package/lib/AutoComplete.mjs.map +1 -0
  3. package/lib/Checkbox.mjs +76 -0
  4. package/lib/Checkbox.mjs.map +1 -0
  5. package/lib/Combobox.mjs +544 -0
  6. package/lib/Combobox.mjs.map +1 -0
  7. package/lib/ComboboxWithCustom.mjs +77 -0
  8. package/lib/ComboboxWithCustom.mjs.map +1 -0
  9. package/lib/DatePicker.mjs +625 -0
  10. package/lib/DatePicker.mjs.map +1 -0
  11. package/lib/EditableBox.mjs +496 -0
  12. package/lib/EditableBox.mjs.map +1 -0
  13. package/lib/Errors.mjs +176 -0
  14. package/lib/Errors.mjs.map +1 -0
  15. package/lib/ExpandableBox.mjs +209 -0
  16. package/lib/ExpandableBox.mjs.map +1 -0
  17. package/lib/Field.mjs +65 -0
  18. package/lib/Field.mjs.map +1 -0
  19. package/lib/FieldBox.mjs +80 -0
  20. package/lib/FieldBox.mjs.map +1 -0
  21. package/lib/Form.mjs +322 -0
  22. package/lib/Form.mjs.map +1 -0
  23. package/lib/FormValue.mjs +111 -0
  24. package/lib/FormValue.mjs.map +1 -0
  25. package/lib/InputboxGroup.mjs +104 -0
  26. package/lib/InputboxGroup.mjs.map +1 -0
  27. package/lib/Radiobox.mjs +67 -0
  28. package/lib/Radiobox.mjs.map +1 -0
  29. package/lib/RichText.mjs +634 -0
  30. package/lib/RichText.mjs.map +1 -0
  31. package/lib/TextInput.mjs +359 -0
  32. package/lib/TextInput.mjs.map +1 -0
  33. package/lib/alt/checkbox/SimpleCheckbox.mjs +82 -0
  34. package/lib/alt/checkbox/SimpleCheckbox.mjs.map +1 -0
  35. package/lib/alt/checkbox/ToggleCheckbox.mjs +68 -0
  36. package/lib/alt/checkbox/ToggleCheckbox.mjs.map +1 -0
  37. package/lib/alt/radiobox/CleanTabRadiobox.mjs +44 -0
  38. package/lib/alt/radiobox/CleanTabRadiobox.mjs.map +1 -0
  39. package/lib/alt/radiobox/IconRadioBox.mjs +52 -0
  40. package/lib/alt/radiobox/IconRadioBox.mjs.map +1 -0
  41. package/lib/alt/radiobox/SimpleRadiobox.mjs +61 -0
  42. package/lib/alt/radiobox/SimpleRadiobox.mjs.map +1 -0
  43. package/lib/alt/radiobox/TabRadiobox.mjs +54 -0
  44. package/lib/alt/radiobox/TabRadiobox.mjs.map +1 -0
  45. package/lib/enhanceInput.mjs +131 -0
  46. package/lib/enhanceInput.mjs.map +1 -0
  47. package/lib/index.cjs +5345 -0
  48. package/lib/index.cjs.map +1 -0
  49. package/lib/index.mjs +28 -0
  50. package/lib/index.mjs.map +1 -0
  51. package/lib/useConfirmDialog.mjs +117 -0
  52. package/lib/useConfirmDialog.mjs.map +1 -0
  53. package/lib/useDraftConfirmation.mjs +56 -0
  54. package/lib/useDraftConfirmation.mjs.map +1 -0
  55. package/lib/validation.mjs +91 -0
  56. package/lib/validation.mjs.map +1 -0
  57. package/lib/withFormImageInput.mjs +281 -0
  58. package/lib/withFormImageInput.mjs.map +1 -0
  59. package/package.json +44 -25
  60. package/src/AutoComplete.jsx +642 -0
  61. package/src/Checkbox.jsx +45 -0
  62. package/src/Combobox.jsx +606 -0
  63. package/src/ComboboxWithCustom.jsx +68 -0
  64. package/src/DatePicker.jsx +733 -0
  65. package/src/EditableBox.jsx +502 -0
  66. package/src/Errors.jsx +174 -0
  67. package/src/ExpandableBox.jsx +207 -0
  68. package/src/Field.jsx +76 -0
  69. package/src/FieldBox.jsx +136 -0
  70. package/src/Form.jsx +291 -0
  71. package/src/FormValue.jsx +67 -0
  72. package/src/IconRadioBoxOldHubt.jsx +93 -0
  73. package/src/InputboxGroup.jsx +121 -0
  74. package/src/Radiobox.jsx +33 -0
  75. package/src/RichText.jsx +812 -0
  76. package/src/TextInput.jsx +519 -0
  77. package/src/alt/checkbox/SimpleCheckbox.jsx +162 -0
  78. package/src/alt/checkbox/ToggleCheckbox.jsx +143 -0
  79. package/src/alt/radiobox/CleanTabRadiobox.jsx +46 -0
  80. package/src/alt/radiobox/IconRadioBox.jsx +93 -0
  81. package/src/alt/radiobox/SimpleRadiobox.jsx +153 -0
  82. package/src/alt/radiobox/TabRadiobox.jsx +73 -0
  83. package/src/enhanceInput.jsx +107 -0
  84. package/src/index.js +27 -0
  85. package/src/useConfirmDialog.jsx +105 -0
  86. package/src/useDraftConfirmation.jsx +60 -0
  87. package/src/validation.js +92 -0
  88. package/src/withFormImageInput.jsx +297 -0
  89. package/.babelrc +0 -28
  90. package/lib/index.js +0 -6255
  91. package/rollup.config.js +0 -43
  92. 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
+