Skip to content

Commit 27b0109

Browse files
RohitRaj011AbhishekA1509
authored andcommitted
feat: Add 'KeyValueTable' Component
1 parent 3cbf0cc commit 27b0109

File tree

7 files changed

+453
-4
lines changed

7 files changed

+453
-4
lines changed

src/Common/CustomTagSelector/ResizableTagTextArea.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const ResizableTagTextArea = ({
3232
dependentRef,
3333
dataTestId,
3434
handleKeyDown,
35+
disableOnBlurResizeToMinHeight,
3536
}: ResizableTagTextAreaProps) => {
3637
const [text, setText] = useState('')
3738

@@ -41,7 +42,7 @@ export const ResizableTagTextArea = ({
4142

4243
const handleChange = (event) => {
4344
setText(event.target.value)
44-
onChange(event)
45+
onChange?.(event)
4546
}
4647

4748
const reInitHeight = () => {
@@ -69,9 +70,11 @@ export const ResizableTagTextArea = ({
6970
useThrottledEffect(reInitHeight, 500, [text])
7071

7172
const handleOnBlur = (event) => {
72-
refVar.current.style.height = `${minHeight}px`
73-
if (dependentRef) {
74-
dependentRef.current.style.height = `${minHeight}px`
73+
if (!disableOnBlurResizeToMinHeight) {
74+
refVar.current.style.height = `${minHeight}px`
75+
if (dependentRef) {
76+
dependentRef.current.style.height = `${minHeight}px`
77+
}
7578
}
7679
onBlur && onBlur(event)
7780
}

src/Common/CustomTagSelector/Types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,5 @@ export interface ResizableTagTextAreaProps {
8585
dependentRef?: React.MutableRefObject<HTMLTextAreaElement>
8686
dataTestId?: string
8787
handleKeyDown?: any
88+
disableOnBlurResizeToMinHeight?: boolean
8889
}
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/*
2+
* Copyright (c) 2024 Devtron Inc.
3+
* All rights reserved.
4+
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/*
19+
* Copyright (c) 2024 Devtron Inc.
20+
* All rights reserved.
21+
22+
* Licensed under the Apache License, Version 2.0 (the "License");
23+
* you may not use this file except in compliance with the License.
24+
* You may obtain a copy of the License at
25+
26+
* http://www.apache.org/licenses/LICENSE-2.0
27+
28+
* Unless required by applicable law or agreed to in writing, software
29+
* distributed under the License is distributed on an "AS IS" BASIS,
30+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31+
* See the License for the specific language governing permissions and
32+
* limitations under the License.
33+
*/
34+
35+
import React, { createRef, useEffect, useRef, useState } from 'react'
36+
37+
import { ReactComponent as ICArrowDown } from '../../../Assets/Icon/ic-arrow-down.svg'
38+
import { ReactComponent as ICCross } from '../../../Assets/Icon/ic-cross.svg'
39+
import { ResizableTagTextArea, SortingOrder, useStateFilters } from '../../../Common'
40+
import { KeyValueRow, KeyValueTableProps } from './KeyValueTable.types'
41+
42+
import './KeyValueTable.scss'
43+
44+
export const KeyValueTable = <K extends string>({
45+
config,
46+
maskValue,
47+
isSortable,
48+
headerComponent,
49+
onChange,
50+
onDelete,
51+
}: KeyValueTableProps<K>) => {
52+
// CONSTANTS
53+
const { headers, rows } = config
54+
const firstHeaderKey = headers[0].key
55+
const secondHeaderKey = headers[1].key
56+
57+
// STATES
58+
const [_rows, setRows] = useState<KeyValueRow<K>[]>(rows)
59+
const [newRowAdded, setNewRowAdded] = useState(false)
60+
61+
// HOOKS
62+
const { sortBy, sortOrder, handleSorting } = useStateFilters({
63+
initialSortKey: firstHeaderKey,
64+
})
65+
const inputRowRef = useRef<HTMLTextAreaElement>()
66+
const keyTextAreaRef = useRef<React.RefObject<HTMLTextAreaElement>[]>([])
67+
const valueTextAreaRef = useRef<React.RefObject<HTMLTextAreaElement>[]>([])
68+
69+
useEffect(() => {
70+
if (keyTextAreaRef.current.length !== _rows.length) {
71+
keyTextAreaRef.current = new Array(_rows.length)
72+
.fill(0)
73+
.map((_, i) => keyTextAreaRef.current[i] || createRef<HTMLTextAreaElement>())
74+
}
75+
76+
if (valueTextAreaRef.current.length !== _rows.length) {
77+
valueTextAreaRef.current = new Array(_rows.length)
78+
.fill(0)
79+
.map((_, i) => valueTextAreaRef.current[i] || createRef<HTMLTextAreaElement>())
80+
}
81+
}, [])
82+
83+
useEffect(() => {
84+
const sortFn = (a: KeyValueRow<K>, b: KeyValueRow<K>) => {
85+
if (!a[sortBy].value && !b[sortBy].value) {
86+
return 0
87+
}
88+
if (sortOrder === SortingOrder.ASC) {
89+
return a[sortBy].value < b[sortBy].value ? 1 : -1
90+
}
91+
return a[sortBy].value > b[sortBy].value ? 1 : -1
92+
}
93+
94+
const sortedRows = [..._rows]
95+
sortedRows.sort(sortFn)
96+
setRows(sortedRows)
97+
}, [sortOrder])
98+
99+
useEffect(() => {
100+
const firstRow = _rows?.[0]
101+
if (firstRow && newRowAdded) {
102+
setNewRowAdded(false)
103+
104+
if (
105+
!firstRow[secondHeaderKey].value &&
106+
keyTextAreaRef.current[0].current &&
107+
valueTextAreaRef.current[0].current
108+
) {
109+
keyTextAreaRef.current[0].current.focus()
110+
}
111+
if (
112+
!firstRow[firstHeaderKey].value &&
113+
valueTextAreaRef.current[0].current &&
114+
valueTextAreaRef.current[0].current
115+
) {
116+
valueTextAreaRef.current[0].current.focus()
117+
}
118+
}
119+
}, [_rows, newRowAdded])
120+
121+
// METHODS
122+
const onSortBtnClick = () => handleSorting(sortBy)
123+
124+
const onNewRowEdit = (key: K) => (e: React.ChangeEvent<HTMLTextAreaElement>) => {
125+
const { value } = e.target
126+
127+
const data = {
128+
[firstHeaderKey]: {
129+
value: key === firstHeaderKey ? value : '',
130+
placeholder: 'Enter Key',
131+
},
132+
[secondHeaderKey]: {
133+
value: key === secondHeaderKey ? value : '',
134+
placeholder: 'Enter Value',
135+
},
136+
} as KeyValueRow<K>
137+
138+
setNewRowAdded(true)
139+
setRows([data, ..._rows])
140+
keyTextAreaRef.current = [createRef(), ...keyTextAreaRef.current]
141+
valueTextAreaRef.current = [createRef(), ...valueTextAreaRef.current]
142+
}
143+
144+
const onRowDataEdit =
145+
(row: KeyValueRow<K>, key: K, rowIndex: number) => (e: React.ChangeEvent<HTMLTextAreaElement>) => {
146+
const { value } = e.target
147+
148+
let newRows = []
149+
if (!value && !row[key === firstHeaderKey ? secondHeaderKey : firstHeaderKey].value) {
150+
newRows = _rows.filter((_, idx) => idx !== rowIndex)
151+
152+
keyTextAreaRef.current = keyTextAreaRef.current.filter((_, idx) => idx !== rowIndex)
153+
valueTextAreaRef.current = valueTextAreaRef.current.filter((_, idx) => idx !== rowIndex)
154+
if (inputRowRef.current) {
155+
inputRowRef.current.focus()
156+
}
157+
} else {
158+
newRows = [
159+
..._rows.slice(0, rowIndex),
160+
{
161+
...row,
162+
[key]: {
163+
...row[key],
164+
value,
165+
},
166+
},
167+
..._rows.slice(rowIndex + 1),
168+
]
169+
}
170+
setRows(newRows)
171+
onChange?.(rowIndex, key)
172+
}
173+
174+
return (
175+
<div style={{ minHeight: '500px', background: 'white', padding: '2px' }}>
176+
<div className="dc__border br-4 w-100 table-container">
177+
<div
178+
className="table-row flexbox dc__align-items-center bcn-50 dc__border-bottom"
179+
style={{ borderColor: 'var(--N100)' }}
180+
>
181+
{headers.map(({ key, label, className }) =>
182+
isSortable && key === firstHeaderKey ? (
183+
<button
184+
key={key}
185+
type="button"
186+
className={`dc__unset-button-styles cn-9 fs-13 lh-20 py-8 px-12 fw-6 flexbox dc__align-items-center dc__gap-2 head__key ${className || ''}`}
187+
onClick={onSortBtnClick}
188+
>
189+
{label}
190+
<ICArrowDown
191+
className="icon-dim-16 fcn-7 rotate cursor"
192+
style={{
193+
['--rotateBy' as string]: sortOrder === SortingOrder.ASC ? '0deg' : '180deg',
194+
}}
195+
/>
196+
</button>
197+
) : (
198+
<div
199+
key={key}
200+
className={`cn-9 fs-13 lh-20 py-8 px-12 fw-6 flexbox dc__align-items-center dc__gap-2 ${key === firstHeaderKey ? 'head__key' : 'flex-grow-1'} ${className || ''}`}
201+
>
202+
{label}
203+
</div>
204+
),
205+
)}
206+
{!!headerComponent && <div className="px-12">{headerComponent}</div>}
207+
</div>
208+
<div
209+
className="table-row flexbox dc__align-items-center dc__border-bottom"
210+
style={{ borderColor: 'var(--N100)' }}
211+
>
212+
{headers.map(({ key }) => (
213+
<div
214+
key={key}
215+
className={`cn-9 fs-13 lh-20 py-8 px-12 flex dc__overflow-auto ${key === firstHeaderKey ? 'head__key' : 'flex-grow-1'}`}
216+
>
217+
<textarea
218+
ref={key === firstHeaderKey ? inputRowRef : undefined}
219+
className="table-input table-input__text-area pt-8 pb-8 pl-10 pb-10 lh-20 fs-13 fw-4"
220+
value=""
221+
rows={1}
222+
placeholder={key === firstHeaderKey ? 'Enter Key' : 'Enter Value'}
223+
onChange={onNewRowEdit(key)}
224+
/>
225+
</div>
226+
))}
227+
</div>
228+
{_rows?.map((row, index) => (
229+
<div
230+
key={`${index.toString()}`}
231+
className={`table-row flexbox dc__align-items-center ${index !== _rows.length - 1 ? 'dc__border-bottom' : ''}`}
232+
style={{ borderColor: 'var(--N100)' }}
233+
>
234+
{headers.map(({ key }, i) => (
235+
<div
236+
key={`${index.toString()}-${i.toString()}`}
237+
className={`cn-9 fs-13 lh-20 py-8 px-12 dc__overflow-auto flexbox dc__align-items-center dc__gap-4 ${key === firstHeaderKey ? 'head__key' : 'flex-grow-1'}`}
238+
>
239+
{maskValue?.[key] && row[key].value ? (
240+
'*****'
241+
) : (
242+
<>
243+
<ResizableTagTextArea
244+
{...row[key]}
245+
className="table-input"
246+
minHeight={20}
247+
maxHeight={144}
248+
value={row[key].value}
249+
onChange={onRowDataEdit(row, key, index)}
250+
refVar={
251+
key === firstHeaderKey
252+
? keyTextAreaRef.current[index]
253+
: valueTextAreaRef.current[index]
254+
}
255+
dependentRef={
256+
key === firstHeaderKey
257+
? valueTextAreaRef.current[index]
258+
: keyTextAreaRef.current[index]
259+
}
260+
disableOnBlurResizeToMinHeight
261+
/>
262+
{row[key].showAsterisk && (
263+
<span className="cr-5 fs-16 dc__align-self-start px-6">*</span>
264+
)}
265+
</>
266+
)}
267+
</div>
268+
))}
269+
<div className="icon flex dc__no-shrink py-10 px-8">
270+
<ICCross
271+
onClick={(e) => onDelete?.(e, index)}
272+
className="icon-dim-16 fcn-4 dc__align-self-start"
273+
/>
274+
</div>
275+
</div>
276+
))}
277+
</div>
278+
</div>
279+
)
280+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright (c) 2024 Devtron Inc.
3+
* All rights reserved.
4+
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
.head {
19+
&__key {
20+
align-self: stretch;
21+
flex: 0 0 20%;
22+
border-right: 1px solid var(--N100);
23+
}
24+
}
25+
26+
.icon {
27+
border-left: 1px solid var(--N100);
28+
flex-basis: 32px;
29+
align-self: stretch;
30+
}
31+
32+
.table-row {
33+
&:first-child {
34+
border-top-left-radius: 4px;
35+
border-top-right-radius: 4px;
36+
}
37+
38+
&:last-child {
39+
border-bottom-left-radius: 4px;
40+
border-bottom-right-radius: 4px;
41+
}
42+
}
43+
44+
.table-input {
45+
outline: none;
46+
border: none;
47+
padding: 0;
48+
width: 100%;
49+
background: inherit;
50+
51+
&__text-area {
52+
resize: none;
53+
border-radius: 4px;
54+
outline: none;
55+
}
56+
}

0 commit comments

Comments
 (0)