Skip to content

Commit 8700076

Browse files
authored
Merge pull request #1191 from RedisInsight/feature/RI-3470_Formatters_improvements
Feature/ri 3470 formatters improvements
2 parents 1fddc0d + 1c1de96 commit 8700076

File tree

7 files changed

+205
-14
lines changed

7 files changed

+205
-14
lines changed

redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.spec.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,66 @@ describe('InlineItemEditor', () => {
3939
render(<InlineItemEditor {...instance(mockedProps)} onDecline={jest.fn()} />)
4040
expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toHaveFocus()
4141
})
42+
43+
describe('approveByValidation', () => {
44+
it('should not render popover after click on Apply btn if approveByValidation return "true" in the props and onApply should be called', () => {
45+
const approveByValidationMock = jest.fn().mockReturnValue(true)
46+
const onApplyMock = jest.fn().mockReturnValue(false)
47+
const { queryByTestId } = render(
48+
<InlineItemEditor
49+
{...instance(mockedProps)}
50+
onApply={onApplyMock}
51+
onDecline={jest.fn()}
52+
approveByValidation={approveByValidationMock}
53+
/>
54+
)
55+
56+
fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value: 'val123' } })
57+
58+
fireEvent.click(screen.getByTestId(/apply-btn/))
59+
expect(queryByTestId('approve-popover')).not.toBeInTheDocument()
60+
expect(onApplyMock).toBeCalled()
61+
})
62+
63+
it('should render popover after click on Apply btn if approveByValidation return "false" in the props and onApply should not be called ', () => {
64+
const approveByValidationMock = jest.fn().mockReturnValue(false)
65+
const onApplyMock = jest.fn().mockReturnValue(false)
66+
const { queryByTestId } = render(
67+
<InlineItemEditor
68+
{...instance(mockedProps)}
69+
onApply={onApplyMock}
70+
onDecline={jest.fn()}
71+
approveByValidation={approveByValidationMock}
72+
/>
73+
)
74+
75+
fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value: 'val123' } })
76+
77+
fireEvent.click(screen.getByTestId(/apply-btn/))
78+
expect(queryByTestId('approve-popover')).toBeInTheDocument()
79+
expect(onApplyMock).not.toBeCalled()
80+
})
81+
82+
it('should render popover after click on Apply btn if approveByValidation return "false" in the props and onApply should be called after click on Save btn', () => {
83+
const approveByValidationMock = jest.fn().mockReturnValue(false)
84+
const onApplyMock = jest.fn().mockReturnValue(false)
85+
const { queryByTestId } = render(
86+
<InlineItemEditor
87+
{...instance(mockedProps)}
88+
onApply={onApplyMock}
89+
onDecline={jest.fn()}
90+
approveByValidation={approveByValidationMock}
91+
/>
92+
)
93+
94+
fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value: 'val123' } })
95+
96+
fireEvent.click(screen.getByTestId(/apply-btn/))
97+
expect(queryByTestId('approve-popover')).toBeInTheDocument()
98+
expect(onApplyMock).not.toBeCalled()
99+
100+
fireEvent.click(screen.getByTestId(/save-btn/))
101+
expect(onApplyMock).toBeCalled()
102+
})
103+
})
42104
})

redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.tsx

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
EuiFocusTrap,
1717
EuiWindowEvent,
1818
EuiToolTip,
19+
EuiPopover,
20+
EuiButton,
21+
EuiText,
1922
} from '@elastic/eui'
2023
import { IconSize } from '@elastic/eui/src/components/icon/icon'
2124
import styles from './styles.module.scss'
@@ -25,7 +28,7 @@ type Design = 'default' | 'separate'
2528

2629
export interface Props {
2730
onDecline: (event?: React.MouseEvent<HTMLElement>) => void
28-
onApply: (value: string, event: React.MouseEvent<HTMLFormElement, MouseEvent>) => void
31+
onApply: (value: string, event: React.MouseEvent) => void
2932
onChange?: (value: string) => void
3033
fieldName?: string
3134
initialValue?: string
@@ -49,6 +52,8 @@ export interface Props {
4952
disabledTooltipText?: { title: string, text: string }
5053
preventOutsideClick?: boolean
5154
disableFocusTrap?: boolean
55+
approveByValidation?: (value: string) => boolean
56+
approveText?: { title: string, text: string }
5257
}
5358

5459
const InlineItemEditor = (props: Props) => {
@@ -77,11 +82,14 @@ const InlineItemEditor = (props: Props) => {
7782
controlsClassName,
7883
disabledTooltipText,
7984
preventOutsideClick = false,
80-
disableFocusTrap = false
85+
disableFocusTrap = false,
86+
approveByValidation,
87+
approveText,
8188
} = props
8289
const containerEl: Ref<HTMLDivElement> = useRef(null)
8390
const [value, setValue] = useState<string>(initialValue)
8491
const [isError, setIsError] = useState<boolean>(false)
92+
const [isShowApprovePopover, setIsShowApprovePopover] = useState(false)
8593

8694
const inputRef: Ref<HTMLInputElement> = useRef(null)
8795

@@ -132,8 +140,17 @@ const InlineItemEditor = (props: Props) => {
132140
}
133141
}
134142

135-
const handleFormSubmit = (event: React.MouseEvent<HTMLFormElement, MouseEvent>): void => {
143+
const handleApplyClick = (event: React.MouseEvent) => {
144+
if (approveByValidation && !approveByValidation?.(value)) {
145+
setIsShowApprovePopover(true)
146+
} else {
147+
handleFormSubmit(event)
148+
}
149+
}
150+
151+
const handleFormSubmit = (event: React.MouseEvent<HTMLElement>): void => {
136152
event.preventDefault()
153+
event.stopPropagation()
137154
onApply(value, event)
138155
}
139156

@@ -152,10 +169,10 @@ const InlineItemEditor = (props: Props) => {
152169
iconSize={iconSize ?? 'l'}
153170
iconType="check"
154171
color="primary"
155-
type="submit"
156172
aria-label="Apply"
157173
className={cx(styles.btn, styles.applyBtn)}
158174
isDisabled={isDisabledApply()}
175+
onClick={handleApplyClick}
159176
data-testid="apply-btn"
160177
/>
161178
</EuiToolTip>
@@ -218,7 +235,45 @@ const InlineItemEditor = (props: Props) => {
218235
isDisabled={isLoading}
219236
data-testid="cancel-btn"
220237
/>
221-
{ApplyBtn}
238+
{!approveByValidation && ApplyBtn}
239+
{approveByValidation && (
240+
<EuiPopover
241+
anchorPosition="leftCenter"
242+
isOpen={isShowApprovePopover}
243+
closePopover={() => setIsShowApprovePopover(false)}
244+
anchorClassName={styles.popoverAnchor}
245+
panelClassName={cx(styles.popoverPanel)}
246+
className={styles.popoverWrapper}
247+
button={ApplyBtn}
248+
>
249+
<div className={styles.popover} data-testid="approve-popover">
250+
<EuiText size="m">
251+
{!!approveText?.title && (
252+
<h4>
253+
<b>{approveText?.title}</b>
254+
</h4>
255+
)}
256+
<EuiText size="s" color="subdued" className={styles.approveText}>
257+
{approveText?.text}
258+
</EuiText>
259+
</EuiText>
260+
<div className={styles.popoverFooter}>
261+
<EuiButton
262+
fill
263+
color="warning"
264+
aria-label="Save"
265+
className={cx(styles.btn, styles.saveBtn)}
266+
isDisabled={isDisabledApply()}
267+
onClick={handleFormSubmit}
268+
data-testid="save-btn"
269+
>
270+
Save
271+
</EuiButton>
272+
</div>
273+
</div>
274+
275+
</EuiPopover>
276+
)}
222277
</div>
223278
</EuiForm>
224279
</EuiFocusTrap>

redisinsight/ui/src/components/inline-item-editor/styles.module.scss

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323
z-index: 1;
2424

2525
.tooltip,
26-
:global(.euiButtonIcon) {
26+
.declineBtn,
27+
.popoverWrapper {
2728
width: 50% !important;
2829
height: 100% !important;
2930
}
3031
}
3132

32-
.tooltip :global(.euiButtonIcon) {
33+
.applyBtn {
34+
height: 100% !important;
3335
width: 100% !important;
3436
}
3537

@@ -76,14 +78,19 @@
7678
width: 60px;
7779
z-index: 4;
7880

79-
.btn {
81+
.btn,
82+
.popoverWrapper {
8083
margin: 6px 3px;
8184
height: 24px !important;
8285
width: 24px !important;
86+
}
87+
88+
.btn:hover {
89+
background-color: var(--hoverInListColorDarken) !important;
90+
}
8391

84-
&:hover {
85-
background-color: var(--hoverInListColorDarken) !important;
86-
}
92+
.applyBtn {
93+
margin-top: 0;
8794
}
8895

8996
svg {
@@ -109,3 +116,47 @@
109116
margin-right: 80px;
110117
word-break: break-all;
111118
}
119+
120+
.popoverAnchor,
121+
.popoverWrapper .tooltip {
122+
width: 100% !important;
123+
height: 100% !important;
124+
}
125+
126+
.popoverPanel:global(.euiPanel--paddingMedium) {
127+
width: 296px !important;
128+
padding: 24px 30px !important;
129+
border: 1px solid var(--euiColorPrimary) !important;
130+
background-color: var(--browserTableRowEven) !important;
131+
132+
:global(.euiPopover__panelArrow:after) {
133+
border-left-color: var(--browserTableRowEven) !important;
134+
}
135+
:global(.euiPopover__panelArrow:before) {
136+
border-left-color: var(--euiColorPrimary) !important;
137+
}
138+
}
139+
140+
.popover {
141+
word-wrap: break-word;
142+
143+
h4 b {
144+
font-size: 14px !important;
145+
color: var(--htmlColor) !important;
146+
}
147+
}
148+
.approveText {
149+
font-size: 13px !important;
150+
letter-spacing: -0.13px;
151+
}
152+
153+
.popoverFooter {
154+
display: flex;
155+
justify-content: flex-end;
156+
margin-top: 12px;
157+
158+
.saveBtn {
159+
height: 36px !important;
160+
width: 86px !important;
161+
}
162+
}

redisinsight/ui/src/constants/browser.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@ export const TEXT_UNPRINTABLE_CHARACTERS = {
44
title: 'Non-printable characters have been detected',
55
text: 'Use Workbench or CLI to edit without data loss.',
66
}
7-
87
export const TEXT_DISABLED_FORMATTER_EDITING = 'Cannot edit the value in this format'
8+
9+
export const TEXT_INVALID_VALUE = {
10+
title: 'Value will be saved as Unicode',
11+
text: 'as it is not valid in the selected format.',
12+
}

redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
KeyTypes,
4545
OVER_RENDER_BUFFER_COUNT,
4646
TableCellAlignment,
47+
TEXT_INVALID_VALUE,
4748
TEXT_DISABLED_FORMATTER_EDITING,
4849
TEXT_UNPRINTABLE_CHARACTERS
4950
} from 'uiSrc/constants'
@@ -350,6 +351,12 @@ const HashDetails = (props: Props) => {
350351
controlsClassName={styles.textAreaControls}
351352
onDecline={() => handleEditField(rowIndex, false)}
352353
onApply={() => handleApplyEditField(fieldItem)}
354+
approveText={TEXT_INVALID_VALUE}
355+
approveByValidation={() =>
356+
formattingBuffer(
357+
stringToSerializedBufferFormat(viewFormat, areaValue),
358+
viewFormat
359+
)?.isValid}
353360
>
354361
<EuiTextArea
355362
fullWidth

redisinsight/ui/src/pages/browser/components/list-details/ListDetails.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
KeyTypes,
2828
OVER_RENDER_BUFFER_COUNT,
2929
TableCellAlignment,
30+
TEXT_INVALID_VALUE,
3031
TEXT_DISABLED_FORMATTER_EDITING,
3132
TEXT_UNPRINTABLE_CHARACTERS
3233
} from 'uiSrc/constants'
@@ -296,6 +297,12 @@ const ListDetails = (props: Props) => {
296297
disabledTooltipText={TEXT_UNPRINTABLE_CHARACTERS}
297298
onDecline={() => handleEditElement(index, false)}
298299
onApply={() => handleApplyEditElement(index)}
300+
approveText={TEXT_INVALID_VALUE}
301+
approveByValidation={() =>
302+
formattingBuffer(
303+
stringToSerializedBufferFormat(viewFormat, areaValue),
304+
viewFormat
305+
)?.isValid}
299306
>
300307
<EuiTextArea
301308
fullWidth

redisinsight/ui/src/pages/browser/components/string-details/StringDetails.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ import {
3030
import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor'
3131
import { AddStringFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config'
3232
import { selectedKeyDataSelector, selectedKeySelector } from 'uiSrc/slices/browser/keys'
33-
34-
import { TEXT_UNPRINTABLE_CHARACTERS } from 'uiSrc/constants'
33+
import { TEXT_INVALID_VALUE, TEXT_UNPRINTABLE_CHARACTERS } from 'uiSrc/constants'
3534
import { calculateTextareaLines } from 'uiSrc/utils/calculateTextareaLines'
3635

3736
import styles from './styles.module.scss'
@@ -177,6 +176,12 @@ const StringDetails = (props: Props) => {
177176
onDecline={onDeclineChanges}
178177
onApply={onApplyChanges}
179178
declineOnUnmount={false}
179+
approveText={TEXT_INVALID_VALUE}
180+
approveByValidation={() =>
181+
formattingBuffer(
182+
stringToSerializedBufferFormat(viewFormat, areaValue),
183+
viewFormat
184+
)?.isValid}
180185
>
181186
<EuiTextArea
182187
fullWidth

0 commit comments

Comments
 (0)