-
Notifications
You must be signed in to change notification settings - Fork 472
RI-8222 Add "Add element" to the array View tab #6129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: be/RI-8222/add-array-append-endpoint
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| export const ARRAY_ADD_FORM_TEST_ID = 'array-add-form' | ||
|
|
||
| export const VALUE_LABEL = 'Value' | ||
| export const INDEX_LABEL = 'Index' | ||
| export const INDEX_PLACEHOLDER = 'Leave empty to append to the end' | ||
| export const INDEX_HINT = | ||
| 'Leave empty to append the value to the end of the array. Enter an index to ' + | ||
| 'set the value at that exact position (overwriting any existing value there).' | ||
| export const INVALID_INDEX_MESSAGE = | ||
| 'Index must be an integer string between 0 and 18446744073709551614' | ||
| export const ADD_BUTTON_LABEL = 'Add' | ||
| export const CANCEL_BUTTON_LABEL = 'Cancel' | ||
|
|
||
| export const CONFIRM_TITLE = 'Add element to a production database?' | ||
| export const CONFIRM_DESCRIPTION = | ||
| 'You are about to add an element to a key on a production database.' | ||
| export const CONFIRM_BUTTON_TEXT = 'Add' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| import React from 'react' | ||
| import { | ||
| act, | ||
| fireEvent, | ||
| initialStateDefault, | ||
| mockStore, | ||
| render, | ||
| screen, | ||
| waitFor, | ||
| } from 'uiSrc/utils/test-utils' | ||
| import { apiService } from 'uiSrc/services' | ||
| import { stringToBuffer } from 'uiSrc/utils' | ||
|
|
||
| import { ArrayAddForm } from './ArrayAddForm' | ||
|
|
||
| const keyProp = stringToBuffer('mykey') | ||
|
|
||
| // The form's key must be the live selected key, otherwise applyArrayWriteResult | ||
| // suppresses the success side effects (onSuccess/closePanel) as a stale write. | ||
| const stateWithKeySelected = { | ||
| ...initialStateDefault, | ||
| app: { | ||
| ...initialStateDefault.app, | ||
| context: { | ||
| ...initialStateDefault.app.context, | ||
| browser: { | ||
| ...initialStateDefault.app.context.browser, | ||
| keyList: { | ||
| ...initialStateDefault.app.context.browser.keyList, | ||
| selectedKey: keyProp, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| const renderForm = (closePanel = jest.fn()) => | ||
| render(<ArrayAddForm keyProp={keyProp} closePanel={closePanel} />, { | ||
| store: mockStore(stateWithKeySelected), | ||
| }) | ||
|
|
||
| const findCall = (fragment: string) => | ||
| (apiService.post as jest.Mock).mock.calls.find(([url]) => | ||
| (url as string).includes(fragment), | ||
| ) | ||
|
|
||
| describe('ArrayAddForm', () => { | ||
| beforeEach(() => { | ||
| apiService.post = jest.fn().mockResolvedValue({ status: 200, data: {} }) | ||
| }) | ||
|
|
||
| it('renders the value and index inputs', () => { | ||
| renderForm() | ||
| expect(screen.getByTestId('array-add-form-value')).toBeInTheDocument() | ||
| expect(screen.getByTestId('array-add-form-index')).toBeInTheDocument() | ||
| }) | ||
|
|
||
| it('appends (POST /array/append) when the index is left empty', async () => { | ||
| const closePanel = jest.fn() | ||
| renderForm(closePanel) | ||
|
|
||
| fireEvent.change(screen.getByTestId('array-add-form-value'), { | ||
| target: { value: 'hello' }, | ||
| }) | ||
| fireEvent.click(screen.getByTestId('array-add-form-submit')) | ||
|
|
||
| await waitFor(() => { | ||
| expect(findCall('array/append')).toBeTruthy() | ||
| }) | ||
| expect(findCall('array/set-element')).toBeFalsy() | ||
| expect(closePanel).toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('does not close the panel if the form unmounted before the write resolved', async () => { | ||
| // Hold the write open so we can unmount (key switch + fresh panel) first. | ||
| let resolvePost: (value: unknown) => void = () => {} | ||
| apiService.post = jest.fn().mockImplementation( | ||
| () => | ||
| new Promise((resolve) => { | ||
| resolvePost = resolve | ||
| }), | ||
| ) | ||
| const closePanel = jest.fn() | ||
| const { unmount } = renderForm(closePanel) | ||
|
|
||
| fireEvent.change(screen.getByTestId('array-add-form-value'), { | ||
| target: { value: 'hello' }, | ||
| }) | ||
| fireEvent.click(screen.getByTestId('array-add-form-submit')) | ||
|
|
||
| unmount() | ||
| await act(async () => { | ||
| resolvePost({ status: 200, data: {} }) | ||
| }) | ||
|
|
||
| // The stale success callback must not close the now-current panel. | ||
| expect(closePanel).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('sets at index (POST /array/set-element) when an index is provided', async () => { | ||
| renderForm() | ||
|
|
||
| fireEvent.change(screen.getByTestId('array-add-form-value'), { | ||
| target: { value: 'hello' }, | ||
| }) | ||
| fireEvent.change(screen.getByTestId('array-add-form-index'), { | ||
| target: { value: '5' }, | ||
| }) | ||
| fireEvent.click(screen.getByTestId('array-add-form-submit')) | ||
|
|
||
| await waitFor(() => { | ||
| const call = findCall('array/set-element') | ||
| expect(call).toBeTruthy() | ||
| expect((call?.[1] as { index: string }).index).toBe('5') | ||
| }) | ||
| expect(findCall('array/append')).toBeFalsy() | ||
| }) | ||
|
|
||
| it('disables Add for a non-canonical index', () => { | ||
| renderForm() | ||
|
|
||
| act(() => { | ||
| fireEvent.change(screen.getByTestId('array-add-form-index'), { | ||
| target: { value: '007' }, | ||
| }) | ||
| }) | ||
|
|
||
| expect(screen.getByTestId('array-add-form-submit')).toBeDisabled() | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| import React, { useEffect, useRef, useState } from 'react' | ||
|
|
||
| import { useAppDispatch, useAppSelector } from 'uiSrc/slices/hooks' | ||
| import { selectedKeySelector } from 'uiSrc/slices/browser/keys' | ||
| import { appendArrayElement, addArrayElement } from 'uiSrc/slices/browser/array' | ||
| import { | ||
| BrowserConfirmationCommandId, | ||
| useProductionWriteConfirmation, | ||
| } from 'uiSrc/components/production-write-confirmation' | ||
| import { stringToSerializedBufferFormat } from 'uiSrc/utils' | ||
| import { parseArrayIndex } from 'uiSrc/utils/arrayIndex' | ||
| import { FormField } from 'uiSrc/components/base/forms/FormField' | ||
| import { TextInput } from 'uiSrc/components/base/inputs' | ||
| import { Col, FlexItem, Row } from 'uiSrc/components/base/layout/flex' | ||
| import { | ||
| PrimaryButton, | ||
| SecondaryButton, | ||
| } from 'uiSrc/components/base/forms/buttons' | ||
|
|
||
| import { EntryContent } from '../../common/AddKeysContainer.styled' | ||
| import { | ||
| ARRAY_ADD_FORM_TEST_ID as TEST_ID, | ||
| ADD_BUTTON_LABEL, | ||
| CANCEL_BUTTON_LABEL, | ||
| CONFIRM_BUTTON_TEXT, | ||
| CONFIRM_DESCRIPTION, | ||
| CONFIRM_TITLE, | ||
| INDEX_HINT, | ||
| INDEX_LABEL, | ||
| INDEX_PLACEHOLDER, | ||
| INVALID_INDEX_MESSAGE, | ||
| VALUE_LABEL, | ||
| } from './ArrayAddForm.constants' | ||
| import { ArrayAddFormProps } from './ArrayAddForm.types' | ||
|
|
||
| /** | ||
| * Content of the "Add element" slide-out panel (rendered inside the shared | ||
| * `AddKeysContainer`, matching List / Vector Set). The index is optional: | ||
| * leaving it empty appends to the end (POST /array/append, ARSET at the current | ||
| * length); providing one sets at that index (POST /array/set-element). | ||
| * `ARINSERT` is intentionally not used — see docs/array-modify-vertical-plan.md. | ||
| */ | ||
| export const ArrayAddForm = ({ keyProp, closePanel }: ArrayAddFormProps) => { | ||
| const dispatch = useAppDispatch() | ||
| const { viewFormat } = useAppSelector(selectedKeySelector) | ||
| const { requestConfirmation } = useProductionWriteConfirmation() | ||
|
|
||
| const [value, setValue] = useState('') | ||
| const [index, setIndex] = useState('') | ||
|
|
||
| // A write resolves asynchronously and the slice still fires onSuccess while | ||
| // its target key is selected. But the user may have closed this panel (key | ||
| // switch, then reopened a fresh panel) before it resolved — this instance is | ||
| // unmounted, and running closePanel would discard the *current* panel. Ignore | ||
| // the callback once this form is gone. | ||
| const isMounted = useRef(true) | ||
| useEffect( | ||
| () => () => { | ||
| isMounted.current = false | ||
| }, | ||
| [], | ||
| ) | ||
|
|
||
| // The index input is optional (empty → append). When provided it must be a | ||
| // canonical decimal string, matching the backend @IsArrayIndex validator. | ||
| const trimmedIndex = index.trim() | ||
| const indexInvalid = | ||
| trimmedIndex.length > 0 && parseArrayIndex(trimmedIndex) !== trimmedIndex | ||
|
|
||
| const handleSuccess = () => { | ||
| if (!isMounted.current) { | ||
| return | ||
| } | ||
| setValue('') | ||
| setIndex('') | ||
| closePanel() | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing add success telemetryLow Severity After a successful append or set-element, Reviewed by Cursor Bugbot for commit 8e5b8c9. Configure here. |
||
|
|
||
| const handleAdd = () => { | ||
| requestConfirmation({ | ||
| title: CONFIRM_TITLE, | ||
| actionDescription: CONFIRM_DESCRIPTION, | ||
| confirmButtonText: CONFIRM_BUTTON_TEXT, | ||
| commandId: BrowserConfirmationCommandId.AddArrayElements, | ||
| disableConfirmationInput: true, | ||
| onConfirm: () => { | ||
| const serialized = stringToSerializedBufferFormat(viewFormat, value) | ||
| if (trimmedIndex.length === 0) { | ||
| dispatch( | ||
| appendArrayElement( | ||
| { key: keyProp, value: serialized }, | ||
| handleSuccess, | ||
| ), | ||
| ) | ||
| } else { | ||
| dispatch( | ||
| addArrayElement( | ||
| { key: keyProp, index: trimmedIndex, value: serialized }, | ||
| handleSuccess, | ||
| ), | ||
| ) | ||
| } | ||
| }, | ||
| }) | ||
| } | ||
|
|
||
| return ( | ||
| <Col gap="m"> | ||
| <EntryContent gap="m" data-testid={TEST_ID}> | ||
| <Row align="end" gap="m"> | ||
| <FlexItem grow> | ||
| <FormField label={VALUE_LABEL}> | ||
| <TextInput | ||
| value={value} | ||
| onChange={setValue} | ||
| placeholder="Enter value" | ||
| data-testid={`${TEST_ID}-value`} | ||
| /> | ||
| </FormField> | ||
| </FlexItem> | ||
| <FlexItem> | ||
| <FormField | ||
| label={INDEX_LABEL} | ||
| infoIconProps={{ content: INDEX_HINT }} | ||
| > | ||
| <TextInput | ||
| value={index} | ||
| onChange={setIndex} | ||
| placeholder={INDEX_PLACEHOLDER} | ||
| error={indexInvalid ? INVALID_INDEX_MESSAGE : undefined} | ||
| data-testid={`${TEST_ID}-index`} | ||
| /> | ||
| </FormField> | ||
| </FlexItem> | ||
| </Row> | ||
| </EntryContent> | ||
|
|
||
| <Row justify="end" gap="m" grow={false}> | ||
| <FlexItem grow={false}> | ||
| <SecondaryButton | ||
| onClick={() => closePanel(true)} | ||
| data-testid={`${TEST_ID}-cancel`} | ||
| > | ||
| {CANCEL_BUTTON_LABEL} | ||
| </SecondaryButton> | ||
| </FlexItem> | ||
| <FlexItem grow={false}> | ||
| <PrimaryButton | ||
| onClick={handleAdd} | ||
| disabled={indexInvalid} | ||
| data-testid={`${TEST_ID}-submit`} | ||
| > | ||
| {ADD_BUTTON_LABEL} | ||
| </PrimaryButton> | ||
| </FlexItem> | ||
| </Row> | ||
| </Col> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' | ||
|
|
||
| export interface ArrayAddFormProps { | ||
| /** The array key being viewed; the element is added to it. */ | ||
| keyProp: RedisResponseBuffer | ||
| /** Closes the add panel. `isCancelled` distinguishes an explicit Cancel from | ||
| * a close-after-success (mirrors the other types' add panels). */ | ||
| closePanel: (isCancelled?: boolean) => void | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { ArrayAddForm } from './ArrayAddForm' |


Uh oh!
There was an error while loading. Please reload this page.