feat: support multiple having key value pairs

This commit is contained in:
Yunus M 2025-05-14 21:08:47 +05:30 committed by ahrefabhi
parent f866047529
commit 1e8dd7b401

View File

@ -1,6 +1,15 @@
import { Button, Select } from 'antd';
import {
autocompletion,
closeCompletion,
CompletionContext,
completionKeymap,
CompletionResult,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { copilot } from '@uiw/codemirror-theme-copilot';
import CodeMirror, { EditorView, keymap } from '@uiw/react-codemirror';
import { useQueryBuilderV2Context } from 'components/QueryBuilderV2/QueryBuilderV2Context';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
const havingOperators = [
{
@ -37,14 +46,29 @@ const havingOperators = [
},
];
function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
const conjunctions = [
{ label: 'AND', value: 'AND' },
{ label: 'OR', value: 'OR' },
];
const openBrace = { label: '(', value: '(' };
const closeBrace = { label: ')', value: ')' };
function HavingFilter(): JSX.Element {
const { aggregationOptions } = useQueryBuilderV2Context();
const [input, setInput] = useState('');
const [selectedHavingOptions, setSelectedHavingOptions] = useState<string[]>(
[],
);
const [cursorPos, setCursorPos] = useState(0);
console.log('selectedHavingOptions', selectedHavingOptions);
const editorRef = useRef<EditorView | null>(null);
console.log('cursorPos', cursorPos);
// Update cursor position on every editor update
const handleUpdate = (update: { view: EditorView }): void => {
const pos = update.view.state.selection.main.from;
setCursorPos(pos);
};
const [options, setOptions] = useState<{ label: string; value: string }[]>([]);
@ -58,8 +82,8 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
const operator = havingOperators[j];
options.push({
label: `${opt.func}(${opt.arg}) ${operator.label}`,
value: `${opt.func}(${opt.arg}) ${operator.label}`,
label: `${opt.func}(${opt.arg}) ${operator.label} `,
value: `${opt.func}(${opt.arg}) ${operator.label} `,
});
}
}
@ -67,21 +91,101 @@ function HavingFilter({ onClose }: { onClose: () => void }): JSX.Element {
setOptions(options);
}, [aggregationOptions]);
console.log('options', options);
// Helper to check if a string is a number
const isNumber = (token: string): boolean => /^-?\d+(\.\d+)?$/.test(token);
const havingAutocomplete = useMemo(() => {
const isKeyOperator = (token: string): boolean =>
options.some((opt) => token.startsWith(opt.value));
// Helper to count standalone ( and ) for grouping
const countGroupingBraces = (
input: string,
): { openCount: number; closeCount: number } => {
// Remove aggregator function calls (e.g., sum(duration))
const withoutFuncs = input.replace(/\w+\([^)]*\)/g, '');
const openCount = (withoutFuncs.match(/\(/g) || []).length;
const closeCount = (withoutFuncs.match(/\)/g) || []).length;
return { openCount, closeCount };
};
return autocompletion({
override: [
(context: CompletionContext): CompletionResult | null => {
const text = context.state.sliceDoc(0, context.pos);
const trimmedText = text.trim();
const tokens = trimmedText.split(/\s+/).filter(Boolean);
const { openCount, closeCount } = countGroupingBraces(text);
// Suggest key/operator pairs and ( for grouping
if (
tokens.length === 0 ||
conjunctions.some((c) => tokens[tokens.length - 1] === c.value) ||
tokens[tokens.length - 1] === '('
) {
return {
from: context.pos,
options: [openBrace, ...options],
};
}
if (isKeyOperator(tokens[tokens.length - 1])) {
return {
from: context.pos,
options: [{ label: 'Enter a number value', type: 'text', apply: '' }],
};
}
// Suggest ) for grouping after a value and a space, if there are unmatched (
if (
tokens.length > 0 &&
isNumber(tokens[tokens.length - 1]) &&
text.endsWith(' ')
) {
return {
from: context.pos,
options:
openCount > closeCount ? [closeBrace, ...conjunctions] : conjunctions,
};
}
return null;
},
],
defaultKeymap: true,
closeOnBlur: false,
maxRenderedOptions: 50,
activateOnTyping: true,
});
}, [options]);
return (
<div className="having-filter-container">
<Select
mode="multiple"
options={options}
onChange={(value): void => {
setSelectedHavingOptions(value);
}}
style={{ width: '100%' }}
/>
<Button className="close-btn periscope-btn ghost" onClick={onClose}>
Close
</Button>
<div className="query-aggregation-select-container">
<CodeMirror
value={input}
onChange={setInput}
className="query-aggregation-select-editor"
width="100%"
theme={copilot}
extensions={[
havingAutocomplete,
javascript({ jsx: false, typescript: false }),
keymap.of([
...completionKeymap,
{
key: 'Escape',
run: closeCompletion,
},
]),
]}
placeholder="Type aggregator functions like sum(), count_distinct(...), etc."
basicSetup={{
lineNumbers: false,
autocompletion: true,
completionKeymap: true,
}}
onUpdate={handleUpdate}
ref={editorRef}
/>
</div>
</div>
);
}