1
1
// This file is part of React-Invenio-Forms
2
2
// Copyright (C) 2020 CERN.
3
3
// Copyright (C) 2020 Northwestern University.
4
- //
5
4
// React-Invenio-Forms is free software; you can redistribute it and/or modify it
6
5
// under the terms of the MIT License; see LICENSE file for more details.
7
6
8
7
import React , { Component , useState } from "react" ;
9
8
import PropTypes from "prop-types" ;
10
9
import { Field , FastField } from "formik" ;
11
- import { Accordion , Container , Icon } from "semantic-ui-react" ;
10
+ import { Accordion , Container , Icon , Label } from "semantic-ui-react" ;
12
11
import _omit from "lodash/omit" ;
13
- import _get from "lodash/get " ;
12
+ import { flattenAndCategorizeErrors } from "../utils " ;
14
13
15
14
export class AccordionField extends Component {
16
- hasError ( errors , initialValues = undefined , values = undefined ) {
17
- const { includesPaths } = this . props ;
18
- for ( const errorPath in errors ) {
19
- for ( const subPath in errors [ errorPath ] ) {
20
- const path = `${ errorPath } .${ subPath } ` ;
21
- if (
22
- _get ( initialValues , path , "" ) === _get ( values , path , "" ) &&
23
- includesPaths . includes ( `${ errorPath } .${ subPath } ` )
24
- )
25
- return true ;
15
+ // Checks if there are any errors that match the given paths.
16
+
17
+ hasError ( errors , includesPaths ) {
18
+ return Object . keys ( errors ) . some ( ( errorPath ) =>
19
+ includesPaths . some ( ( path ) => errorPath . startsWith ( path ) )
20
+ ) ;
21
+ }
22
+
23
+ // Generates a summary of errors categorized by severity.
24
+ getErrorSummary = ( errors , includePaths , severityChecks ) => {
25
+ const count = { } ;
26
+
27
+ // Count generic errors
28
+ for ( const path in errors . flattenedErrors ) {
29
+ if ( includePaths . some ( ( includePath ) => path . startsWith ( includePath ) ) ) {
30
+ count [ "error" ] = ( count [ "error" ] || 0 ) + 1 ;
26
31
}
27
32
}
28
- return false ;
29
- }
33
+
34
+ // Count severity-based errors
35
+ for ( const key in errors . severityChecks ) {
36
+ const severity = errors . severityChecks [ key ] ?. severity ;
37
+ const path = key ;
38
+
39
+ if (
40
+ severity &&
41
+ includePaths . some ( ( includePath ) => path . startsWith ( includePath ) )
42
+ ) {
43
+ count [ severity ] = ( count [ severity ] || 0 ) + 1 ;
44
+ }
45
+ }
46
+
47
+ // Format output to display labels
48
+ // e.g., { error: "1 Error", warning: "2 Warnings" }
49
+ const formattedCount = { } ;
50
+ for ( const [ severity , num ] of Object . entries ( count ) ) {
51
+ const label =
52
+ severityChecks ?. [ severity ] ?. label ||
53
+ severity . charAt ( 0 ) . toUpperCase ( ) + severity . slice ( 1 ) ;
54
+ formattedCount [ severity ] = `${ num } ${ label } ${ num === 1 ? "" : "s" } ` ;
55
+ }
56
+
57
+ return formattedCount ;
58
+ } ;
30
59
31
60
renderAccordion = ( props ) => {
32
61
const {
33
- form : { errors, status , initialErrors, initialValues , values } ,
62
+ form : { errors, initialErrors } ,
34
63
} = props ;
64
+ const { includesPaths, label, children, active, severityChecks } = this . props ;
65
+
66
+ const uiProps = _omit ( this . props , [ "optimized" , "includesPaths" ] ) ;
67
+
68
+ // Merge initial and current errors for accurate validation
69
+ const persistentErrors = { ...initialErrors , ...errors } ;
70
+ const categorizedErrors = flattenAndCategorizeErrors ( persistentErrors ) ;
71
+
72
+ // Determine if the accordion should show an "error" state
73
+ const errorClass = this . hasError ( categorizedErrors . flattenedErrors , includesPaths )
74
+ ? "error"
75
+ : "" ;
76
+
77
+ // Generate summary of errors for display
78
+ const errorSummary = this . getErrorSummary (
79
+ categorizedErrors ,
80
+ includesPaths ,
81
+ severityChecks
82
+ ) ;
35
83
36
- // eslint-disable-next-line no-unused-vars
37
- const { label, children, active, ...ui } = this . props ;
38
- const uiProps = _omit ( { ...ui } , [ "optimized" , "includesPaths" ] ) ;
39
- const hasError = status
40
- ? this . hasError ( status )
41
- : this . hasError ( errors ) || this . hasError ( initialErrors , initialValues , values ) ;
42
- const panels = [
43
- {
44
- key : `panel-${ label } ` ,
45
- title : {
46
- content : label ,
47
- } ,
48
- content : {
49
- content : < Container > { children } </ Container > ,
50
- } ,
51
- } ,
52
- ] ;
53
-
54
- const errorClass = hasError ? "error secondary" : "" ;
55
84
const [ activeIndex , setActiveIndex ] = useState ( active ? 0 : - 1 ) ;
56
85
57
86
const handleTitleClick = ( e , { index } ) => {
@@ -61,37 +90,47 @@ export class AccordionField extends Component {
61
90
return (
62
91
< Accordion
63
92
inverted
64
- className = { `invenio-accordion-field ${ errorClass } ` }
93
+ className = { `invenio-accordion-field ${ errorClass } secondary ` }
65
94
{ ...uiProps }
66
95
>
67
- { panels . map ( ( panel , index ) => (
68
- < React . Fragment key = { panel . key } >
69
- < Accordion . Title
70
- active = { activeIndex === index }
71
- index = { index }
72
- onClick = { handleTitleClick }
73
- onKeyDown = { ( e ) => {
74
- if ( e . key === "Enter" || e . key === " " ) {
75
- handleTitleClick ( e , { index } ) ;
76
- }
77
- } }
78
- tabIndex = { 0 }
96
+ { /* Accordion Title with Error Summary */ }
97
+ < Accordion . Title
98
+ active = { activeIndex === 0 }
99
+ index = { 0 }
100
+ onClick = { handleTitleClick }
101
+ onKeyDown = { ( e ) => {
102
+ if ( e . key === "Enter" || e . key === " " ) {
103
+ handleTitleClick ( e , { index : 0 } ) ;
104
+ }
105
+ } }
106
+ tabIndex = { 0 }
107
+ >
108
+ { label }
109
+ { /* Display error labels */ }
110
+ { Object . entries ( errorSummary ) . map ( ( [ severity , text ] ) => (
111
+ < Label
112
+ key = { severity }
113
+ size = "tiny"
114
+ circular
115
+ className = { `accordion-label ${ severity } ` }
79
116
>
80
- { panel . title . content }
81
- < Icon name = "angle right" />
82
- </ Accordion . Title >
83
- < Accordion . Content active = { activeIndex === index } >
84
- { panel . content . content }
85
- </ Accordion . Content >
86
- </ React . Fragment >
87
- ) ) }
117
+ { text }
118
+ </ Label >
119
+ ) ) }
120
+ { /* Toggle Icon */ }
121
+ < Icon name = { activeIndex === 0 ? "angle down" : "angle right" } />
122
+ </ Accordion . Title >
123
+
124
+ { /* Accordion Content */ }
125
+ < Accordion . Content active = { activeIndex === 0 } >
126
+ < Container > { children } </ Container >
127
+ </ Accordion . Content >
88
128
</ Accordion >
89
129
) ;
90
130
} ;
91
131
92
132
render ( ) {
93
133
const { optimized } = this . props ;
94
-
95
134
const FormikField = optimized ? FastField : Field ;
96
135
return < FormikField name = "" component = { this . renderAccordion } /> ;
97
136
}
@@ -104,6 +143,7 @@ AccordionField.propTypes = {
104
143
optimized : PropTypes . bool ,
105
144
children : PropTypes . node ,
106
145
ui : PropTypes . object ,
146
+ severityChecks : PropTypes . object ,
107
147
} ;
108
148
109
149
AccordionField . defaultProps = {
@@ -113,4 +153,5 @@ AccordionField.defaultProps = {
113
153
optimized : false ,
114
154
children : null ,
115
155
ui : null ,
156
+ severityChecks : null ,
116
157
} ;
0 commit comments