Skip to content
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

CB-3753-1809-fr-add-field-hints-in-the-filter-field #3151

Merged
merged 59 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
2e5066c
CB-3753 adds base for input autocomplete
sergeyteleshev Dec 19, 2024
93c659f
CB-5373 adds InputAutocompletion base
sergeyteleshev Dec 20, 2024
f2014aa
CB-3753 removes unneeded code + fixes bugs with current word selection
sergeyteleshev Dec 23, 2024
9e6401f
CB-3753 replaces with existing menu
sergeyteleshev Dec 23, 2024
88378ee
CB-3753 refactor
sergeyteleshev Dec 23, 2024
6a21f77
CB-3753 fixes menu visibility
sergeyteleshev Dec 23, 2024
6a39b70
Merge branch 'devel' into CB-3753-1809-fr-add-field-hints-in-the-filt…
sergeyteleshev Dec 26, 2024
08286d6
CB-3753 adds autocomplete for where filters from result set column names
sergeyteleshev Dec 26, 2024
862b3c1
CB-3753 cleanup
sergeyteleshev Dec 26, 2024
47fe4b3
CB-3753 cleanup
sergeyteleshev Dec 26, 2024
4a6736d
CB-3753 adds base hints
sergeyteleshev Dec 26, 2024
61eb21b
Merge branch 'devel' into CB-3753-1809-fr-add-field-hints-in-the-filt…
sergeyteleshev Jan 8, 2025
918885c
Merge branch 'devel' into CB-3753-1809-fr-add-field-hints-in-the-filt…
sergeyteleshev Jan 10, 2025
8bde5b8
CB-3753 fixes bug with not available arrow option selection
sergeyteleshev Jan 10, 2025
3582469
CB-3753 cleanup
sergeyteleshev Jan 10, 2025
c9225f2
CB-3753 pr fixes
sergeyteleshev Jan 10, 2025
92e6714
CB-3753 removes redundant useEffect with visibility logic
sergeyteleshev Jan 10, 2025
13e9636
Revert "CB-3753 removes redundant useEffect with visibility logic"
sergeyteleshev Jan 10, 2025
43e6450
CB-3753 adds fuzzy search with Fuse.js library
sergeyteleshev Jan 13, 2025
5777823
CB-3753 cleanup
sergeyteleshev Jan 13, 2025
092b101
CB-3753 adds context input ref
sergeyteleshev Jan 13, 2025
1c65f0a
CB-3753 fixes bugs
sergeyteleshev Jan 13, 2025
19d13ba
CB-3753 removes unused lib in core-utils
sergeyteleshev Jan 13, 2025
9ea0e07
Merge branch 'devel' into CB-3753-1809-fr-add-field-hints-in-the-filt…
sergeyteleshev Jan 14, 2025
7362ca9
CB-3753 pr fixes
sergeyteleshev Jan 14, 2025
0687044
CB-3753 replaces fuse.js with minisearch
sergeyteleshev Jan 14, 2025
2affb81
CB-3753 fixes styles for menu
sergeyteleshev Jan 14, 2025
71b23cf
Merge branch 'devel' into CB-3753-1809-fr-add-field-hints-in-the-filt…
sergeyteleshev Jan 14, 2025
5d3d2b3
CB-3753 moves InputAutocompletionMenu out of the InlineEditor
sergeyteleshev Jan 14, 2025
a713650
CB-3753 adds positioning to the contextInput suggestions via button a…
sergeyteleshev Jan 15, 2025
1cfbaec
CB-3753 cleanup
sergeyteleshev Jan 15, 2025
e83dad3
CB-3753 fixes jumping menu
sergeyteleshev Jan 16, 2025
36d041e
CB-3753 cleanup
sergeyteleshev Jan 16, 2025
07f39c1
CB-3753 fixes bug with incorrect replacement
sergeyteleshev Jan 16, 2025
a1bce28
CB-3753 cleanup
sergeyteleshev Jan 16, 2025
93a2b2b
CB-3753 adds useSearch hook
sergeyteleshev Jan 16, 2025
89baa7b
CB-3753 cleanup
sergeyteleshev Jan 16, 2025
268f94e
Merge branch 'devel' into CB-3753-1809-fr-add-field-hints-in-the-filt…
sergeyteleshev Jan 20, 2025
6076646
CB-3753 pr fixes
sergeyteleshev Jan 20, 2025
433c7d0
Merge branch 'devel' into CB-3753-1809-fr-add-field-hints-in-the-filt…
sergeyteleshev Jan 21, 2025
e0902d6
CB-3753 adds contextMenuPosition instead of mouseContextMenu + makes …
sergeyteleshev Jan 21, 2025
8550b97
Merge branch 'devel' into CB-3753-1809-fr-add-field-hints-in-the-filt…
sergeyteleshev Jan 23, 2025
c26eedf
CB-3753 pr fixes
sergeyteleshev Jan 23, 2025
22c7cec
CB-3753 cleanup
sergeyteleshev Jan 23, 2025
6dc2b36
CB-3753 cleanup
sergeyteleshev Jan 23, 2025
3cd81a0
CB-3753 cleanup
sergeyteleshev Jan 24, 2025
732ca11
CB-3753 cleanup
sergeyteleshev Jan 24, 2025
d65fde6
Revert "CB-3753 cleanup"
sergeyteleshev Jan 24, 2025
4ba97c1
CB-3753 pr fixes
sergeyteleshev Jan 24, 2025
0ca177c
CB-3753 cleanup
sergeyteleshev Jan 24, 2025
2711f9e
CB-3753 pr fixes
sergeyteleshev Jan 24, 2025
d22075f
CB-3753 pr fixes
sergeyteleshev Jan 27, 2025
cf4acf4
CB-3753 pr fixes
sergeyteleshev Jan 27, 2025
49dad5a
CB-3753 cleanup
sergeyteleshev Jan 27, 2025
a279714
Merge branch 'devel' into CB-3753-1809-fr-add-field-hints-in-the-filt…
dariamarutkina Jan 27, 2025
6731fdf
CB-3753 adds word separator + few new DB keywords to suggestions
sergeyteleshev Jan 27, 2025
e31b7ab
Merge branch 'devel' into CB-3753-1809-fr-add-field-hints-in-the-filt…
mr-anton-t Jan 27, 2025
d6c6e4a
CB-3753 fixes filter when applying autocomplete
sergeyteleshev Jan 28, 2025
16fee5f
Merge branch 'devel' into CB-3753-1809-fr-add-field-hints-in-the-filt…
dariamarutkina Jan 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion webapp/packages/core-blocks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@
"@cloudbeaver/core-theming": "^0",
"@cloudbeaver/core-utils": "^0",
"go-split": "^3",
"minisearch": "^7",
"mobx": "^6",
"mobx-react-lite": "^4",
"react": "^18",
"react-hotkeys-hook": "^4",
"reakit": "^1",
"reakit-utils": "^0"
"reakit-utils": "^0",
"react-minisearch": "^7"
},
"peerDependencies": {},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
.menuItem {
composes: theme-ripple from global;
}

.menu {
composes: theme-text-on-surface theme-background-surface theme-typography--caption theme-elevation-z3 from global;
display: flex;
Expand All @@ -18,34 +14,35 @@
outline: none;
z-index: 999;
border-radius: var(--theme-form-element-radius);
}

& .menuItem {
background: transparent;
display: flex;
flex-direction: row;
align-items: center;
padding: 8px 12px;
text-align: left;
outline: none;
color: inherit;
cursor: pointer;
gap: 8px;
.menuItem {
composes: theme-ripple from global;
background: transparent;
display: flex;
flex-direction: row;
align-items: center;
padding: 8px 12px;
text-align: left;
outline: none;
color: inherit;
cursor: pointer;
gap: 8px;

& .itemIcon,
& .itemTitle {
position: relative;
}
& .itemIcon,
& .itemTitle {
position: relative;
}

& .itemIcon {
width: 16px;
height: 16px;
overflow: hidden;
flex-shrink: 0;
& .itemIcon {
width: 16px;
height: 16px;
overflow: hidden;
flex-shrink: 0;

& .iconOrImage {
width: 100%;
height: 100%;
}
& .iconOrImage {
width: 100%;
height: 100%;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/

.menu {
display: none !important;
}
Wroud marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';

import { debounce } from '@cloudbeaver/core-utils';

import BaseDropdownStyles from '../FormControls/BaseDropdown.module.css';
import { getComputed } from '../getComputed.js';
import { IconOrImage } from '../IconOrImage.js';
import { Menu } from '../Menu/Menu.js';
import { MenuItem } from '../Menu/MenuItem.js';
import type { IMenuState } from '../Menu/MenuStateContext.js';
import { s } from '../s.js';
import { Text } from '../Text.js';
import { useObservableRef } from '../useObservableRef.js';
import { useS } from '../useS.js';
import style from './InputAutocompletionMenu.module.css';
import { type InputAutocompleteProposal } from './useInputAutocomplete.js';

interface AutocompletionProps {
proposals: InputAutocompleteProposal[];
inputRef: React.RefObject<HTMLInputElement | HTMLTextAreaElement>;
className?: string;
onSelect?: (proposal: InputAutocompleteProposal) => void;
}

const CONTEXT_INPUT_OFFSET_Y = 3;
const DEBOUNCE_DELAY = 300;

export const InputAutocompletionMenu = observer(function InputAutocompletionMenu({ className, proposals, inputRef, onSelect }: AutocompletionProps) {
const styles = useS(style, BaseDropdownStyles);
const menuRef = useRef<IMenuState>();
const state = useObservableRef(
() => ({
x: 0,
y: 0,
inputValue: '',
}),
{
x: observable.ref,
y: observable.ref,
inputValue: observable.ref,
},
false,
);
const contextMenuPosition = getComputed(() => ({
position: { x: state.x, y: state.y },
handleContextMenuOpen: () => {},
}));

function handleSelect(proposal: InputAutocompleteProposal) {
menuRef.current?.hide();
onSelect?.(proposal);
}

function handleKeyDown(event: any) {
switch (event.key) {
case 'Escape':
menuRef.current?.hide();
break;
case 'ArrowDown':
case 'ArrowUp':
menuRef.current?.first();
break;
default:
break;
}
}

const handleInput = useMemo(
() =>
debounce((event: any) => {
state.inputValue = event.target.value;
}, DEBOUNCE_DELAY),
[state],
);

useLayoutEffect(() => {
if (!inputRef.current) {
return;
}

const inputElement = inputRef.current;
const caretPosition = inputElement.selectionStart || 0;
const inputStyle = window.getComputedStyle(inputElement);

const span = document.createElement('span');
span.style.position = 'absolute';
span.style.visibility = 'hidden';
span.style.whiteSpace = 'pre';
span.style.fontFamily = inputStyle.fontFamily;
span.style.fontSize = inputStyle.fontSize;
span.style.fontWeight = inputStyle.fontWeight;
span.style.letterSpacing = inputStyle.letterSpacing;
span.style.lineHeight = inputStyle.lineHeight;
span.textContent = inputElement.value.slice(0, caretPosition);

document.body.appendChild(span);
const spanRect = span.getBoundingClientRect();
const letterWidth = spanRect.width / span.textContent.length;
document.body.removeChild(span);

state.x = spanRect.width + letterWidth;
state.y = spanRect.height + CONTEXT_INPUT_OFFSET_Y;
}, [state.inputValue, inputRef.current]);

useEffect(() => {
inputRef.current?.addEventListener('keydown', handleKeyDown);
inputRef.current?.addEventListener('input', handleInput);

return () => {
inputRef.current?.removeEventListener('keydown', handleKeyDown);
inputRef.current?.removeEventListener('input', handleInput);
};
}, [inputRef.current]);
Wroud marked this conversation as resolved.
Show resolved Hide resolved

if (!proposals.length) {
return;
}

return (
sergeyteleshev marked this conversation as resolved.
Show resolved Hide resolved
<Menu
contextMenuPosition={contextMenuPosition}
visible={proposals.length > 0}
panelAvailable={proposals.length > 0}
className={s(styles, { menu: true }, className)}
menuRef={menuRef}
label="Autocompletion"
items={proposals.map(proposal => (
<MenuItem
key={proposal.displayString}
id={proposal.displayString}
type="button"
title={proposal.title}
className={s(styles, { menuItem: true })}
onClick={event => handleSelect(proposal)}
>
{proposal.icon && (
<div className={s(styles, { itemIcon: true })}>
<IconOrImage icon={proposal.icon} className={s(styles, { iconOrImage: true })} />
</div>
)}
<Text truncate>{proposal.displayString}</Text>
</MenuItem>
))}
modal
/>
);
});
139 changes: 139 additions & 0 deletions webapp/packages/core-blocks/src/FormControls/useInputAutocomplete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { action, computed, observable } from 'mobx';
import { type RefObject, useEffect, useMemo } from 'react';

import { debounce, isNotNullDefined } from '@cloudbeaver/core-utils';

import { useObservableRef } from '../useObservableRef.js';
import { type SearchStrategy, useSearch } from '../useSearch.js';

export type InputAutocompleteStrategy = 'startsWith' | 'contains' | 'fuzzy';

interface InputAutocompleteOptions {
sourceHints: InputAutocompleteProposal[];
matchStrategy?: SearchStrategy;
predicate?: (suggestion: InputAutocompleteProposal, lastWord?: string) => boolean;
}

export interface InputAutocompleteProposal {
displayString: string;
replacementString: string;
icon?: string;
title?: string;
}

interface State {
replaceCurrentWord: (replacement: string) => void;
currentWord: string;
proposals: InputAutocompleteProposal[];
}

const INPUT_DELAY = 300;
const SEARCH_FIELDS: Array<keyof InputAutocompleteProposal> = ['displayString', 'replacementString'];

export const useInputAutocomplete = (
inputRef: RefObject<HTMLInputElement | HTMLTextAreaElement>,
{ sourceHints, matchStrategy = 'contains', predicate }: InputAutocompleteOptions,
): Readonly<State> => {
const search = useSearch({
sourceHints,
searchFields: SEARCH_FIELDS,
matchStrategy,
predicate,
});

const state = useObservableRef(
() => ({
isFound: false,
input: inputRef.current?.value as string | undefined,
selectionStart: inputRef.current?.selectionStart ?? null,
Wroud marked this conversation as resolved.
Show resolved Hide resolved
replaceCurrentWord(replacement: string) {
const cursorPosition = this.selectionStart;
const words = this.input?.split(' ');

if (!this.currentWord || !isNotNullDefined(words) || !isNotNullDefined(cursorPosition)) {
return;
}

const start = cursorPosition - this.currentWord.length;
const end = cursorPosition;

this.input = this.input?.slice(0, start) + replacement + this.input?.slice(end);
this.selectionStart = start + replacement.length;

if (this.inputRef.current) {
this.inputRef.current.value = this.input;
this.inputRef.current.focus();
this.inputRef.current.setSelectionRange(this.selectionStart, this.selectionStart);
}

this.isFound = true;
},
get currentWord(): string {
const cursorPosition = this.selectionStart;

if (!cursorPosition) {
return '';
}

const substring = this.input?.slice(0, cursorPosition);

if (!substring) {
return '';
}

return substring.split(' ').at(-1) ?? '';
},
get proposals() {
if (this.isFound || !this.currentWord) {
return [];
}

return this.search.searchResult ?? [];
},
}),
{
proposals: computed,
input: observable.ref,
selectionStart: observable.ref,
isFound: observable.ref,
currentWord: computed,
replaceCurrentWord: action.bound,
},
{ sourceHints, matchStrategy, inputRef, search },
);

const handleInput = useMemo(
() =>
debounce((event: Event) => {
const target = event.target as HTMLInputElement;

state.selectionStart = target.selectionStart;
state.input = target.value;
state.isFound = false;
state.search.setSearch(state.currentWord);
}, INPUT_DELAY),
[state],
);

useEffect(() => {
const input = state.inputRef.current;

if (!input) {
return;
}

input.addEventListener('input', handleInput);
return () => {
input.removeEventListener('input', handleInput);
};
}, [state.inputRef.current]);

Check warning on line 136 in webapp/packages/core-blocks/src/FormControls/useInputAutocomplete.ts

View check run for this annotation

Jenkins-CI-integration / CheckStyle TypeScript Report

webapp/packages/core-blocks/src/FormControls/useInputAutocomplete.ts#L136

React Hook useEffect has missing dependencies: handleInput and state.inputRef. Either include them or remove the dependency array. Mutable values like state.inputRef.current arent valid dependencies because mutating them doesnt re-render the component. (react-hooks/exhaustive-deps)

return state as Readonly<State>;
};
Loading
Loading