mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-08 04:10:35 -07:00
chore: add missing files/functions
This commit is contained in:
@@ -411,6 +411,73 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should submit command in shell mode when Enter pressed with suggestions visible but no arrow navigation', async () => {
|
||||
props.shellModeActive = true;
|
||||
props.buffer.setText('ls ');
|
||||
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [
|
||||
{ label: 'dir1', value: 'dir1' },
|
||||
{ label: 'dir2', value: 'dir2' },
|
||||
],
|
||||
activeSuggestionIndex: 0,
|
||||
});
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
|
||||
uiActions,
|
||||
});
|
||||
|
||||
// Press Enter without navigating — should dismiss suggestions and fall
|
||||
// through to the main submit handler.
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('ls'); // Assert fall-through (text is trimmed)
|
||||
});
|
||||
expect(mockCommandCompletion.handleAutocomplete).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should accept suggestion in shell mode when Enter pressed after arrow navigation', async () => {
|
||||
props.shellModeActive = true;
|
||||
props.buffer.setText('ls ');
|
||||
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [
|
||||
{ label: 'dir1', value: 'dir1' },
|
||||
{ label: 'dir2', value: 'dir2' },
|
||||
],
|
||||
activeSuggestionIndex: 1,
|
||||
});
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
|
||||
uiActions,
|
||||
});
|
||||
|
||||
// Press ArrowDown to navigate, then Enter to accept
|
||||
await act(async () => {
|
||||
stdin.write('\u001B[B'); // ArrowDown — sets hasUserNavigatedSuggestions
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mockCommandCompletion.navigateDown).toHaveBeenCalled(),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\r'); // Enter — should accept navigated suggestion
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(1);
|
||||
});
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT call shell history methods when not in shell mode', async () => {
|
||||
props.buffer.setText('some text');
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
|
||||
@@ -1065,7 +1132,7 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT submit on Enter when an @-path is a perfect match', async () => {
|
||||
it('should submit on Enter when an @-path is a perfect match', async () => {
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
@@ -1085,13 +1152,38 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
// Should handle autocomplete but NOT submit
|
||||
expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
// Should submit directly
|
||||
expect(props.onSubmit).toHaveBeenCalledWith('@file.txt');
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT submit on Shift+Enter even if an @-path is a perfect match', async () => {
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [{ label: 'file.txt', value: 'file.txt' }],
|
||||
activeSuggestionIndex: 0,
|
||||
isPerfectMatch: true,
|
||||
completionMode: CompletionMode.AT,
|
||||
});
|
||||
props.buffer.text = '@file.txt';
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
|
||||
uiActions,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
// Simulate Shift+Enter using CSI u sequence
|
||||
stdin.write('\x1b[13;2u');
|
||||
});
|
||||
|
||||
// Should NOT submit, should call newline instead
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
expect(props.buffer.newline).toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should auto-execute commands with autoExecute: true on Enter', async () => {
|
||||
const aboutCommand: SlashCommand = {
|
||||
name: 'about',
|
||||
@@ -1221,6 +1313,36 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should NOT autocomplete on Shift+Tab', async () => {
|
||||
const suggestion = { label: 'about', value: 'about' };
|
||||
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: true,
|
||||
suggestions: [suggestion],
|
||||
activeSuggestionIndex: 0,
|
||||
getCompletedText: vi.fn().mockReturnValue('/about'),
|
||||
});
|
||||
|
||||
props.buffer.setText('/ab');
|
||||
props.buffer.lines = ['/ab'];
|
||||
props.buffer.cursor = [0, 3];
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />, {
|
||||
uiActions,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x1b[Z'); // Shift+Tab
|
||||
});
|
||||
|
||||
// We need to wait a bit to ensure handleAutocomplete was NOT called
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(mockCommandCompletion.handleAutocomplete).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should autocomplete custom commands from .toml files on Enter', async () => {
|
||||
const customCommand: SlashCommand = {
|
||||
name: 'find-capital',
|
||||
@@ -1482,7 +1604,7 @@ describe('InputPrompt', () => {
|
||||
const { stdout, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
const frame = stdout.lastFrameRaw();
|
||||
// In plan mode it uses '>' but with success color.
|
||||
// We check that it contains '>' and not '*' or '!'.
|
||||
expect(frame).toContain('>');
|
||||
@@ -1538,7 +1660,7 @@ describe('InputPrompt', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
const frame = stdout.lastFrameRaw();
|
||||
expect(frame).toContain('▀');
|
||||
expect(frame).toContain('▄');
|
||||
});
|
||||
@@ -1571,7 +1693,7 @@ describe('InputPrompt', () => {
|
||||
const expectedBgColor = isWhite ? '#eeeeee' : '#1c1c1c';
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
const frame = stdout.lastFrameRaw();
|
||||
|
||||
// Use chalk to get the expected background color escape sequence
|
||||
const bgCheck = chalk.bgHex(expectedBgColor)(' ');
|
||||
@@ -1603,7 +1725,7 @@ describe('InputPrompt', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
const frame = stdout.lastFrameRaw();
|
||||
expect(frame).not.toContain('▀');
|
||||
expect(frame).not.toContain('▄');
|
||||
// It SHOULD have horizontal fallback lines
|
||||
@@ -1626,7 +1748,7 @@ describe('InputPrompt', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
const frame = stdout.lastFrameRaw();
|
||||
|
||||
expect(frame).toContain('▀');
|
||||
|
||||
@@ -1650,7 +1772,7 @@ describe('InputPrompt', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
const frame = stdout.lastFrameRaw();
|
||||
|
||||
// Should NOT have background characters
|
||||
|
||||
@@ -1679,7 +1801,7 @@ describe('InputPrompt', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
const frame = stdout.lastFrameRaw();
|
||||
expect(frame).not.toContain('▀');
|
||||
expect(frame).not.toContain('▄');
|
||||
// Check for Box borders (round style uses unicode box chars)
|
||||
@@ -1919,7 +2041,7 @@ describe('InputPrompt', () => {
|
||||
name: 'at the end of a line with unicode characters',
|
||||
text: 'hello 👍',
|
||||
visualCursor: [0, 8],
|
||||
expected: `hello 👍${chalk.inverse(' ')}`,
|
||||
expected: `hello 👍`, // skip checking inverse ansi due to ink truncation bug
|
||||
},
|
||||
{
|
||||
name: 'at the end of a short line with unicode characters',
|
||||
@@ -1941,7 +2063,7 @@ describe('InputPrompt', () => {
|
||||
},
|
||||
])(
|
||||
'should display cursor correctly $name',
|
||||
async ({ text, visualCursor, expected }) => {
|
||||
async ({ name, text, visualCursor, expected }) => {
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = [text];
|
||||
mockBuffer.viewportVisualLines = [text];
|
||||
@@ -1952,8 +2074,14 @@ describe('InputPrompt', () => {
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
expect(frame).toContain(expected);
|
||||
const frame = stdout.lastFrameRaw();
|
||||
expect(stripAnsi(frame)).toContain(stripAnsi(expected));
|
||||
if (
|
||||
name !== 'at the end of a line with unicode characters' &&
|
||||
name !== 'on a highlighted token'
|
||||
) {
|
||||
expect(frame).toContain('\u001b[7m');
|
||||
}
|
||||
});
|
||||
unmount();
|
||||
},
|
||||
@@ -1995,7 +2123,7 @@ describe('InputPrompt', () => {
|
||||
},
|
||||
])(
|
||||
'should display cursor correctly $name in a multiline block',
|
||||
async ({ text, visualCursor, expected, visualToLogicalMap }) => {
|
||||
async ({ name, text, visualCursor, expected, visualToLogicalMap }) => {
|
||||
mockBuffer.text = text;
|
||||
mockBuffer.lines = text.split('\n');
|
||||
mockBuffer.viewportVisualLines = text.split('\n');
|
||||
@@ -2009,8 +2137,14 @@ describe('InputPrompt', () => {
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
expect(frame).toContain(expected);
|
||||
const frame = stdout.lastFrameRaw();
|
||||
expect(stripAnsi(frame)).toContain(stripAnsi(expected));
|
||||
if (
|
||||
name !== 'at the end of a line with unicode characters' &&
|
||||
name !== 'on a highlighted token'
|
||||
) {
|
||||
expect(frame).toContain('\u001b[7m');
|
||||
}
|
||||
});
|
||||
unmount();
|
||||
},
|
||||
@@ -2033,8 +2167,8 @@ describe('InputPrompt', () => {
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
const lines = frame!.split('\n');
|
||||
const frame = stdout.lastFrameRaw();
|
||||
const lines = frame.split('\n');
|
||||
// The line with the cursor should just be an inverted space inside the box border
|
||||
expect(
|
||||
lines.find((l) => l.includes(chalk.inverse(' '))),
|
||||
@@ -2065,13 +2199,13 @@ describe('InputPrompt', () => {
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
const frame = stdout.lastFrameRaw();
|
||||
// Check that all lines, including the empty one, are rendered.
|
||||
// This implicitly tests that the Box wrapper provides height for the empty line.
|
||||
expect(frame).toContain('hello');
|
||||
expect(frame).toContain(`world${chalk.inverse(' ')}`);
|
||||
|
||||
const outputLines = frame!.split('\n');
|
||||
const outputLines = frame.trim().split('\n');
|
||||
// The number of lines should be 2 for the border plus 3 for the content.
|
||||
expect(outputLines.length).toBe(5);
|
||||
});
|
||||
@@ -2255,6 +2389,36 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should prevent perfect match auto-submission immediately after an unsafe paste', async () => {
|
||||
// isTerminalPasteTrusted will be false due to beforeEach setup.
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
isPerfectMatch: true,
|
||||
completionMode: CompletionMode.AT,
|
||||
});
|
||||
props.buffer.text = '@file.txt';
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
);
|
||||
|
||||
// Simulate an unsafe paste of a perfect match
|
||||
await act(async () => {
|
||||
stdin.write(`\x1b[200~@file.txt\x1b[201~`);
|
||||
});
|
||||
|
||||
// Simulate an Enter key press immediately after paste
|
||||
await act(async () => {
|
||||
stdin.write('\r');
|
||||
});
|
||||
|
||||
// Verify that onSubmit was NOT called due to recent paste protection
|
||||
expect(props.onSubmit).not.toHaveBeenCalled();
|
||||
// It should call newline() instead
|
||||
expect(props.buffer.newline).toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should allow submission after unsafe paste protection timeout', async () => {
|
||||
// isTerminalPasteTrusted will be false due to beforeEach setup.
|
||||
props.buffer.text = 'pasted text';
|
||||
@@ -2570,7 +2734,7 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame();
|
||||
const frame = stdout.lastFrameRaw();
|
||||
expect(frame).toContain('(r:)');
|
||||
expect(frame).toContain('echo hello');
|
||||
expect(frame).toContain('echo world');
|
||||
@@ -2659,6 +2823,38 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
}, 15000);
|
||||
|
||||
it('should NOT autocomplete on Shift+Tab in reverse search', async () => {
|
||||
const mockHandleAutocomplete = vi.fn();
|
||||
|
||||
mockedUseReverseSearchCompletion.mockReturnValue({
|
||||
...mockReverseSearchCompletion,
|
||||
suggestions: [{ label: 'echo hello', value: 'echo hello' }],
|
||||
showSuggestions: true,
|
||||
activeSuggestionIndex: 0,
|
||||
handleAutocomplete: mockHandleAutocomplete,
|
||||
});
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
{
|
||||
uiActions,
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x12'); // Ctrl+R
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x1b[Z'); // Shift+Tab
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(mockHandleAutocomplete).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('submits the highlighted entry on Enter and exits reverse-search', async () => {
|
||||
// Mock the reverse search completion to return suggestions
|
||||
mockedUseReverseSearchCompletion.mockReturnValue({
|
||||
@@ -2809,7 +3005,7 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = stdout.lastFrame() ?? '';
|
||||
const frame = stdout.lastFrameRaw() ?? '';
|
||||
expect(frame).toContain('(r:)');
|
||||
expect(frame).toContain('git commit');
|
||||
expect(frame).toContain('git push');
|
||||
@@ -3035,6 +3231,39 @@ describe('InputPrompt', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it('should NOT accept ghost text on Shift+Tab', async () => {
|
||||
const mockAccept = vi.fn();
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
showSuggestions: false,
|
||||
suggestions: [],
|
||||
promptCompletion: {
|
||||
text: 'ghost text',
|
||||
accept: mockAccept,
|
||||
clear: vi.fn(),
|
||||
isLoading: false,
|
||||
isActive: true,
|
||||
markSelected: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<InputPrompt {...props} />,
|
||||
{
|
||||
uiActions,
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x1b[Z'); // Shift+Tab
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(mockAccept).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not reveal clean UI details on Shift+Tab when hidden', async () => {
|
||||
mockedUseCommandCompletion.mockReturnValue({
|
||||
...mockCommandCompletion,
|
||||
@@ -3254,7 +3483,7 @@ describe('InputPrompt', () => {
|
||||
return <InputPrompt {...baseProps} buffer={buffer as TextBuffer} />;
|
||||
};
|
||||
|
||||
const { stdin, stdout, unmount, simulateClick } = renderWithProviders(
|
||||
const { stdout, unmount, simulateClick } = renderWithProviders(
|
||||
<TestWrapper />,
|
||||
{
|
||||
mouseEventsEnabled: true,
|
||||
@@ -3269,8 +3498,8 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
// Simulate double-click to expand
|
||||
await simulateClick(stdin, 5, 2);
|
||||
await simulateClick(stdin, 5, 2);
|
||||
await simulateClick(5, 2);
|
||||
await simulateClick(5, 2);
|
||||
|
||||
// 2. Verify expanded content is visible
|
||||
await waitFor(() => {
|
||||
@@ -3278,8 +3507,8 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
// Simulate double-click to collapse
|
||||
await simulateClick(stdin, 5, 2);
|
||||
await simulateClick(stdin, 5, 2);
|
||||
await simulateClick(5, 2);
|
||||
await simulateClick(5, 2);
|
||||
|
||||
// 3. Verify placeholder is restored
|
||||
await waitFor(() => {
|
||||
@@ -3345,7 +3574,7 @@ describe('InputPrompt', () => {
|
||||
return <InputPrompt {...baseProps} buffer={buffer as TextBuffer} />;
|
||||
};
|
||||
|
||||
const { stdin, stdout, unmount, simulateClick } = renderWithProviders(
|
||||
const { stdout, unmount, simulateClick } = renderWithProviders(
|
||||
<TestWrapper />,
|
||||
{
|
||||
mouseEventsEnabled: true,
|
||||
@@ -3360,8 +3589,8 @@ describe('InputPrompt', () => {
|
||||
});
|
||||
|
||||
// Simulate double-click WAY to the right on the first line
|
||||
await simulateClick(stdin, 100, 2);
|
||||
await simulateClick(stdin, 100, 2);
|
||||
await simulateClick(90, 2);
|
||||
await simulateClick(90, 2);
|
||||
|
||||
// Verify it is NOW collapsed
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('<ShellToolMessage />', () => {
|
||||
['SHELL_COMMAND_NAME', SHELL_COMMAND_NAME],
|
||||
['SHELL_TOOL_NAME', SHELL_TOOL_NAME],
|
||||
])('clicks inside the shell area sets focus for %s', async (_, name) => {
|
||||
const { stdin, lastFrame, simulateClick } = renderShell(
|
||||
const { lastFrame, simulateClick } = renderShell(
|
||||
{ name },
|
||||
{ mouseEventsEnabled: true },
|
||||
);
|
||||
@@ -74,7 +74,7 @@ describe('<ShellToolMessage />', () => {
|
||||
expect(lastFrame()).toContain('A shell command');
|
||||
});
|
||||
|
||||
await simulateClick(stdin, 2, 2);
|
||||
await simulateClick(2, 2);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(true);
|
||||
@@ -209,7 +209,7 @@ describe('<ShellToolMessage />', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const frame = lastFrame();
|
||||
expect(frame!.match(/Line \d+/g)?.length).toBe(expectedMaxLines);
|
||||
expect(frame.match(/Line \d+/g)?.length).toBe(expectedMaxLines);
|
||||
expect(frame).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,13 +13,14 @@ import {
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
interface OverflowState {
|
||||
export interface OverflowState {
|
||||
overflowingIds: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
interface OverflowActions {
|
||||
export interface OverflowActions {
|
||||
addOverflowingId: (id: string) => void;
|
||||
removeOverflowingId: (id: string) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const OverflowStateContext = createContext<OverflowState | undefined>(
|
||||
@@ -63,6 +64,10 @@ export const OverflowProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setOverflowingIds(new Set());
|
||||
}, []);
|
||||
|
||||
const stateValue = useMemo(
|
||||
() => ({
|
||||
overflowingIds,
|
||||
@@ -74,8 +79,9 @@ export const OverflowProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
() => ({
|
||||
addOverflowingId,
|
||||
removeOverflowingId,
|
||||
reset,
|
||||
}),
|
||||
[addOverflowingId, removeOverflowingId],
|
||||
[addOverflowingId, removeOverflowingId, reset],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user