@shipfox/react-ui 0.30.0 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/code-block/code-content.js +5 -2
- package/dist/components/dashboard/components/kpi-card.js +2 -2
- package/dist/components/shipql-editor/index.d.ts +1 -1
- package/dist/components/shipql-editor/index.js +1 -1
- package/dist/components/shipql-editor/lexical/leaf-close-overlay.js +65 -2
- package/dist/components/shipql-editor/lexical/shipql-plugin.js +10 -1
- package/dist/components/shipql-editor/shipql-editor-inner.d.ts +1 -1
- package/dist/components/shipql-editor/shipql-editor-inner.js +148 -41
- package/dist/components/shipql-editor/shipql-editor.d.ts +12 -0
- package/dist/components/shipql-editor/shipql-editor.js +1 -1
- package/dist/components/shipql-editor/suggestions/generate-suggestions.d.ts +22 -0
- package/dist/components/shipql-editor/suggestions/generate-suggestions.js +170 -0
- package/dist/components/shipql-editor/suggestions/shipql-range-facet-panel.d.ts +9 -0
- package/dist/components/shipql-editor/suggestions/shipql-range-facet-panel.js +376 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestion-item.d.ts +11 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestion-item.js +40 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-dropdown.d.ts +19 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-dropdown.js +128 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-footer.d.ts +11 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-footer.js +123 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-plugin.d.ts +27 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-plugin.js +407 -0
- package/dist/components/shipql-editor/suggestions/types.d.ts +20 -0
- package/dist/components/shipql-editor/suggestions/types.js +3 -0
- package/dist/components/table/data-table.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Icon } from '../../../components/icon/index.js';
|
|
3
|
+
import { Kbd } from '../../../components/kbd/index.js';
|
|
4
|
+
import { cn } from '../../../utils/cn.js';
|
|
5
|
+
function FooterBadge({ label, kbd }) {
|
|
6
|
+
return /*#__PURE__*/ _jsxs("span", {
|
|
7
|
+
className: cn('flex items-center gap-4 text-xs select-none transition-colors duration-100 text-foreground-neutral-muted'),
|
|
8
|
+
children: [
|
|
9
|
+
label,
|
|
10
|
+
/*#__PURE__*/ _jsx(Kbd, {
|
|
11
|
+
className: "h-16 shrink-0",
|
|
12
|
+
children: kbd
|
|
13
|
+
})
|
|
14
|
+
]
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function SyntaxHint({ label, example }) {
|
|
18
|
+
return /*#__PURE__*/ _jsxs("span", {
|
|
19
|
+
className: "flex items-center gap-4 text-xs",
|
|
20
|
+
children: [
|
|
21
|
+
/*#__PURE__*/ _jsx("span", {
|
|
22
|
+
className: "text-foreground-neutral-subtle",
|
|
23
|
+
children: label
|
|
24
|
+
}),
|
|
25
|
+
/*#__PURE__*/ _jsx("span", {
|
|
26
|
+
className: "text-tag-purple-icon",
|
|
27
|
+
children: example
|
|
28
|
+
})
|
|
29
|
+
]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const SYNTAX_HINTS = {
|
|
33
|
+
value: [
|
|
34
|
+
{
|
|
35
|
+
label: 'Operators',
|
|
36
|
+
example: '(a OR b), a AND b'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: 'Negate',
|
|
40
|
+
example: '-a:b, NOT a:b'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: 'Wildcard',
|
|
44
|
+
example: '*'
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
range: [
|
|
48
|
+
{
|
|
49
|
+
label: 'Range',
|
|
50
|
+
example: '[min TO max]'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: 'Operators',
|
|
54
|
+
example: '>=, <=, >, <, ='
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
export function ShipQLSuggestionsFooter({ showValueActions, showSyntaxHelp, onToggleSyntaxHelp, isError, syntaxHintMode }) {
|
|
59
|
+
const syntaxVisible = showSyntaxHelp || isError;
|
|
60
|
+
const hints = SYNTAX_HINTS[syntaxHintMode];
|
|
61
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
62
|
+
className: "shrink-0 flex flex-col bg-background-field-base",
|
|
63
|
+
children: [
|
|
64
|
+
syntaxVisible && /*#__PURE__*/ _jsxs(_Fragment, {
|
|
65
|
+
children: [
|
|
66
|
+
/*#__PURE__*/ _jsx("div", {
|
|
67
|
+
className: "border-t border-border-neutral-base-component"
|
|
68
|
+
}),
|
|
69
|
+
isError && /*#__PURE__*/ _jsx("div", {
|
|
70
|
+
className: "bg-background-accent-error-base/8 px-8 py-4",
|
|
71
|
+
children: /*#__PURE__*/ _jsx("p", {
|
|
72
|
+
className: "text-xs font-medium text-foreground-highlight-error",
|
|
73
|
+
children: "Invalid characters in input"
|
|
74
|
+
})
|
|
75
|
+
}),
|
|
76
|
+
/*#__PURE__*/ _jsx("div", {
|
|
77
|
+
className: "flex flex-wrap items-center gap-x-16 gap-y-8 px-8 py-4",
|
|
78
|
+
children: hints.map((hint)=>/*#__PURE__*/ _jsx(SyntaxHint, {
|
|
79
|
+
label: hint.label,
|
|
80
|
+
example: hint.example
|
|
81
|
+
}, hint.label))
|
|
82
|
+
})
|
|
83
|
+
]
|
|
84
|
+
}),
|
|
85
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
86
|
+
className: "border-t border-border-neutral-base-component flex items-center justify-between gap-12 px-8 py-4",
|
|
87
|
+
children: [
|
|
88
|
+
/*#__PURE__*/ _jsxs("button", {
|
|
89
|
+
onMouseDown: (e)=>{
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
e.stopPropagation();
|
|
92
|
+
onToggleSyntaxHelp();
|
|
93
|
+
},
|
|
94
|
+
className: cn('flex items-center gap-4 px-6 py-2 rounded-6 text-xs font-medium transition-colors cursor-pointer', syntaxVisible ? 'text-foreground-neutral-base hover:bg-background-button-transparent-hover' : 'text-foreground-neutral-muted hover:text-foreground-neutral-subtle hover:bg-background-button-transparent-hover'),
|
|
95
|
+
type: "button",
|
|
96
|
+
children: [
|
|
97
|
+
/*#__PURE__*/ _jsx(Icon, {
|
|
98
|
+
name: "questionLine",
|
|
99
|
+
className: "size-12"
|
|
100
|
+
}),
|
|
101
|
+
"Syntax"
|
|
102
|
+
]
|
|
103
|
+
}),
|
|
104
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
105
|
+
className: "flex items-center gap-4",
|
|
106
|
+
children: [
|
|
107
|
+
showValueActions && /*#__PURE__*/ _jsx(FooterBadge, {
|
|
108
|
+
label: "Negate",
|
|
109
|
+
kbd: "Shift"
|
|
110
|
+
}),
|
|
111
|
+
/*#__PURE__*/ _jsx(FooterBadge, {
|
|
112
|
+
label: "New tag",
|
|
113
|
+
kbd: "Tab"
|
|
114
|
+
})
|
|
115
|
+
]
|
|
116
|
+
})
|
|
117
|
+
]
|
|
118
|
+
})
|
|
119
|
+
]
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=shipql-suggestions-footer.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AstNode } from '@shipfox/shipql-parser';
|
|
2
|
+
import type { LeafAstNode } from '../lexical/shipql-leaf-node';
|
|
3
|
+
import type { FacetDef, SuggestionItem } from './types';
|
|
4
|
+
interface ShipQLSuggestionsPluginProps {
|
|
5
|
+
facets: FacetDef[];
|
|
6
|
+
currentFacet: string | null;
|
|
7
|
+
setCurrentFacet: (facet: string | null) => void;
|
|
8
|
+
valueSuggestions: string[];
|
|
9
|
+
isLoadingValueSuggestions: boolean;
|
|
10
|
+
open: boolean;
|
|
11
|
+
setOpen: (open: boolean) => void;
|
|
12
|
+
selectedIndex: number;
|
|
13
|
+
setSelectedIndex: (i: number) => void;
|
|
14
|
+
items: SuggestionItem[];
|
|
15
|
+
setItems: (items: SuggestionItem[]) => void;
|
|
16
|
+
isSelectingRef: React.RefObject<boolean>;
|
|
17
|
+
applyRef: React.RefObject<((value: string) => void) | null>;
|
|
18
|
+
negationPrefixRef: React.RefObject<string>;
|
|
19
|
+
focusedLeafNode: LeafAstNode | null;
|
|
20
|
+
onLeafChange?: (payload: {
|
|
21
|
+
partialValue: string;
|
|
22
|
+
ast: AstNode | null;
|
|
23
|
+
}) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function ShipQLSuggestionsPlugin({ facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, open, setOpen, selectedIndex, setSelectedIndex, items, setItems, isSelectingRef, applyRef, negationPrefixRef, focusedLeafNode, onLeafChange, }: ShipQLSuggestionsPluginProps): null;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=shipql-suggestions-plugin.d.ts.map
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
2
|
+
import { $createRangeSelection, $createTextNode, $getRoot, $getSelection, $isRangeSelection, $isTextNode, $setSelection, BLUR_COMMAND, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, FOCUS_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND } from 'lexical';
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { $isShipQLLeafNode } from '../lexical/shipql-leaf-node.js';
|
|
5
|
+
import { buildSuggestionItems, detectFacetContext, extractFacetFromLeaf, negationPrefixFromSource, normalizeFacets, stripNegationPrefix, tryParse } from './generate-suggestions.js';
|
|
6
|
+
function getActiveSegment(para) {
|
|
7
|
+
const children = para.getChildren();
|
|
8
|
+
let active = '';
|
|
9
|
+
for(let i = children.length - 1; i >= 0; i--){
|
|
10
|
+
const child = children[i];
|
|
11
|
+
if ($isShipQLLeafNode(child)) {
|
|
12
|
+
// When trailing text starts immediately after a leaf that contains ':',
|
|
13
|
+
// the user is typing a value continuation — e.g.
|
|
14
|
+
// [Leaf("status:"), Text("s")] should yield "status:s", not just "s".
|
|
15
|
+
const leafText = child.getTextContent();
|
|
16
|
+
if (active.length > 0 && active[0] !== ' ' && leafText.includes(':')) {
|
|
17
|
+
active = leafText + active;
|
|
18
|
+
}
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
active = child.getTextContent() + active;
|
|
22
|
+
}
|
|
23
|
+
return active.trimStart();
|
|
24
|
+
}
|
|
25
|
+
export function ShipQLSuggestionsPlugin({ facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, open, setOpen, selectedIndex, setSelectedIndex, items, setItems, isSelectingRef, applyRef, negationPrefixRef, focusedLeafNode, onLeafChange }) {
|
|
26
|
+
const [editor] = useLexicalComposerContext();
|
|
27
|
+
// Stable refs so callbacks always see the latest values without re-registering.
|
|
28
|
+
const openRef = useRef(open);
|
|
29
|
+
openRef.current = open;
|
|
30
|
+
const itemsRef = useRef(items);
|
|
31
|
+
itemsRef.current = items;
|
|
32
|
+
const selectedIndexRef = useRef(selectedIndex);
|
|
33
|
+
selectedIndexRef.current = selectedIndex;
|
|
34
|
+
const setOpenRef = useRef(setOpen);
|
|
35
|
+
setOpenRef.current = setOpen;
|
|
36
|
+
const setSelectedIndexRef = useRef(setSelectedIndex);
|
|
37
|
+
setSelectedIndexRef.current = setSelectedIndex;
|
|
38
|
+
const setItemsRef = useRef(setItems);
|
|
39
|
+
setItemsRef.current = setItems;
|
|
40
|
+
const facetsRef = useRef(facets);
|
|
41
|
+
facetsRef.current = facets;
|
|
42
|
+
const valueSuggestionsRef = useRef(valueSuggestions);
|
|
43
|
+
valueSuggestionsRef.current = valueSuggestions;
|
|
44
|
+
const setCurrentFacetRef = useRef(setCurrentFacet);
|
|
45
|
+
setCurrentFacetRef.current = setCurrentFacet;
|
|
46
|
+
const currentFacetRef = useRef(currentFacet);
|
|
47
|
+
currentFacetRef.current = currentFacet;
|
|
48
|
+
const focusedLeafNodeRef = useRef(focusedLeafNode);
|
|
49
|
+
focusedLeafNodeRef.current = focusedLeafNode;
|
|
50
|
+
const isLoadingValueSuggestionsRef = useRef(isLoadingValueSuggestions);
|
|
51
|
+
isLoadingValueSuggestionsRef.current = isLoadingValueSuggestions;
|
|
52
|
+
const onLeafChangeRef = useRef(onLeafChange);
|
|
53
|
+
onLeafChangeRef.current = onLeafChange;
|
|
54
|
+
const isFocusedRef = useRef(false);
|
|
55
|
+
const hasNavigatedRef = useRef(false);
|
|
56
|
+
const prevFocusedLeafRef = useRef(null);
|
|
57
|
+
// ── Leaf refocus effect ─────────────────────────────────────────────────────
|
|
58
|
+
useEffect(()=>{
|
|
59
|
+
if (focusedLeafNode === prevFocusedLeafRef.current) return;
|
|
60
|
+
prevFocusedLeafRef.current = focusedLeafNode;
|
|
61
|
+
if (focusedLeafNode) {
|
|
62
|
+
if (focusedLeafNode.type === 'text') {
|
|
63
|
+
// Text-type leaves (e.g. "status:") aren't real facet:value nodes.
|
|
64
|
+
// Detect facet context from the text content, mirroring the update
|
|
65
|
+
// listener's handling of text-type leaves.
|
|
66
|
+
const activeText = focusedLeafNode.value;
|
|
67
|
+
const facetCtx = detectFacetContext(activeText, normalizeFacets(facetsRef.current));
|
|
68
|
+
negationPrefixRef.current = facetCtx?.negationPrefix ?? stripNegationPrefix(activeText).prefix;
|
|
69
|
+
setCurrentFacetRef.current(facetCtx?.facet ?? null);
|
|
70
|
+
let text = '';
|
|
71
|
+
editor.getEditorState().read(()=>{
|
|
72
|
+
text = $getRoot().getTextContent();
|
|
73
|
+
});
|
|
74
|
+
onLeafChangeRef.current?.({
|
|
75
|
+
partialValue: facetCtx?.partialValue ?? '',
|
|
76
|
+
ast: tryParse(text)
|
|
77
|
+
});
|
|
78
|
+
const leafItems = buildSuggestionItems(facetsRef.current, valueSuggestionsRef.current, activeText, null);
|
|
79
|
+
setItemsRef.current(leafItems);
|
|
80
|
+
} else {
|
|
81
|
+
const facet = extractFacetFromLeaf(focusedLeafNode);
|
|
82
|
+
negationPrefixRef.current = focusedLeafNode.type === 'not' ? negationPrefixFromSource(focusedLeafNode.source) : '';
|
|
83
|
+
setCurrentFacetRef.current(facet ?? null);
|
|
84
|
+
// Don't call onLeafChange here — the update listener that
|
|
85
|
+
// preceded this rebuild already set the correct partial value.
|
|
86
|
+
const leafItems = buildSuggestionItems(facetsRef.current, valueSuggestionsRef.current, '', focusedLeafNode);
|
|
87
|
+
setItemsRef.current(leafItems);
|
|
88
|
+
}
|
|
89
|
+
hasNavigatedRef.current = false;
|
|
90
|
+
setSelectedIndexRef.current(-1);
|
|
91
|
+
if (isFocusedRef.current) setOpenRef.current(true);
|
|
92
|
+
} else {
|
|
93
|
+
// Leaf deselected — regenerate text-based suggestions immediately
|
|
94
|
+
editor.getEditorState().read(()=>{
|
|
95
|
+
const para = $getRoot().getFirstChild();
|
|
96
|
+
if (!para) return;
|
|
97
|
+
const activeText = getActiveSegment(para);
|
|
98
|
+
const facetCtx = detectFacetContext(activeText, normalizeFacets(facetsRef.current));
|
|
99
|
+
negationPrefixRef.current = facetCtx?.negationPrefix ?? stripNegationPrefix(activeText).prefix;
|
|
100
|
+
setCurrentFacetRef.current(facetCtx?.facet ?? null);
|
|
101
|
+
const text = $getRoot().getTextContent();
|
|
102
|
+
onLeafChangeRef.current?.({
|
|
103
|
+
partialValue: facetCtx?.partialValue ?? '',
|
|
104
|
+
ast: tryParse(text)
|
|
105
|
+
});
|
|
106
|
+
const newItems = buildSuggestionItems(facetsRef.current, valueSuggestionsRef.current, activeText, null);
|
|
107
|
+
setItemsRef.current(newItems);
|
|
108
|
+
hasNavigatedRef.current = false;
|
|
109
|
+
setSelectedIndexRef.current(-1);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}, [
|
|
113
|
+
focusedLeafNode,
|
|
114
|
+
editor,
|
|
115
|
+
negationPrefixRef
|
|
116
|
+
]);
|
|
117
|
+
// ── Reactive regeneration when facets/valueSuggestions change ──────────────
|
|
118
|
+
useEffect(()=>{
|
|
119
|
+
if (!openRef.current) return;
|
|
120
|
+
if (prevFocusedLeafRef.current && prevFocusedLeafRef.current.type !== 'text') {
|
|
121
|
+
const leafItems = buildSuggestionItems(facets, valueSuggestions, '', prevFocusedLeafRef.current);
|
|
122
|
+
setItemsRef.current(leafItems);
|
|
123
|
+
setOpenRef.current(isSelectingRef.current || isFocusedRef.current);
|
|
124
|
+
} else if (prevFocusedLeafRef.current?.type === 'text') {
|
|
125
|
+
// Text-type leaf (e.g. "status:") lives inside a ShipQLLeafNode, so
|
|
126
|
+
// getActiveSegment() would return '' — use the leaf's value instead.
|
|
127
|
+
const activeText = prevFocusedLeafRef.current.value;
|
|
128
|
+
const newItems = buildSuggestionItems(facets, valueSuggestions, activeText, null);
|
|
129
|
+
setItemsRef.current(newItems);
|
|
130
|
+
setOpenRef.current(isSelectingRef.current || isFocusedRef.current);
|
|
131
|
+
} else {
|
|
132
|
+
editor.getEditorState().read(()=>{
|
|
133
|
+
const para = $getRoot().getFirstChild();
|
|
134
|
+
if (!para) return;
|
|
135
|
+
const activeText = getActiveSegment(para);
|
|
136
|
+
const newItems = buildSuggestionItems(facets, valueSuggestions, activeText, null);
|
|
137
|
+
setItemsRef.current(newItems);
|
|
138
|
+
hasNavigatedRef.current = false;
|
|
139
|
+
setSelectedIndexRef.current(-1);
|
|
140
|
+
setOpenRef.current(isSelectingRef.current || isFocusedRef.current);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}, [
|
|
144
|
+
facets,
|
|
145
|
+
valueSuggestions,
|
|
146
|
+
editor,
|
|
147
|
+
isSelectingRef
|
|
148
|
+
]);
|
|
149
|
+
// ── Apply function ──────────────────────────────────────────────────────────
|
|
150
|
+
useEffect(()=>{
|
|
151
|
+
applyRef.current = (selectedValue)=>{
|
|
152
|
+
const isFacetSelection = !currentFacetRef.current;
|
|
153
|
+
editor.update(()=>{
|
|
154
|
+
const para = $getRoot().getFirstChild();
|
|
155
|
+
if (!para) return;
|
|
156
|
+
const insertText = currentFacetRef.current ? `${negationPrefixRef.current}${currentFacetRef.current}:${selectedValue} ` : `${negationPrefixRef.current}${selectedValue}:`;
|
|
157
|
+
// Case 1: Cursor is inside a focused leaf chip — replace the chip in-place
|
|
158
|
+
if (focusedLeafNodeRef.current) {
|
|
159
|
+
const sel = $getSelection();
|
|
160
|
+
if ($isRangeSelection(sel)) {
|
|
161
|
+
const anchor = sel.anchor.getNode();
|
|
162
|
+
if ($isShipQLLeafNode(anchor)) {
|
|
163
|
+
const newNode = $createTextNode(insertText);
|
|
164
|
+
anchor.insertBefore(newNode);
|
|
165
|
+
anchor.remove();
|
|
166
|
+
const newSel = $createRangeSelection();
|
|
167
|
+
const len = newNode.getTextContentSize();
|
|
168
|
+
newSel.anchor.set(newNode.getKey(), len, 'text');
|
|
169
|
+
newSel.focus.set(newNode.getKey(), len, 'text');
|
|
170
|
+
$setSelection(newSel);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Case 2: Cursor is in a plain text node — replace that node's content
|
|
176
|
+
const sel = $getSelection();
|
|
177
|
+
if ($isRangeSelection(sel)) {
|
|
178
|
+
const anchor = sel.anchor.getNode();
|
|
179
|
+
if ($isTextNode(anchor) && !$isShipQLLeafNode(anchor)) {
|
|
180
|
+
const prevSibling = anchor.getPreviousSibling();
|
|
181
|
+
const prevText = prevSibling?.getTextContent() ?? '';
|
|
182
|
+
const needsSpace = prevText.length > 0 && !prevText.endsWith(' ');
|
|
183
|
+
const finalText = needsSpace ? ` ${insertText}` : insertText;
|
|
184
|
+
anchor.setTextContent(finalText);
|
|
185
|
+
const newSel = $createRangeSelection();
|
|
186
|
+
newSel.anchor.set(anchor.getKey(), finalText.length, 'text');
|
|
187
|
+
newSel.focus.set(anchor.getKey(), finalText.length, 'text');
|
|
188
|
+
$setSelection(newSel);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Case 3: Fallback — append after last leaf chip
|
|
193
|
+
const children = para.getChildren();
|
|
194
|
+
let lastLeafIdx = -1;
|
|
195
|
+
for(let i = children.length - 1; i >= 0; i--){
|
|
196
|
+
if ($isShipQLLeafNode(children[i])) {
|
|
197
|
+
lastLeafIdx = i;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for(let i = children.length - 1; i > lastLeafIdx; i--){
|
|
202
|
+
children[i].remove();
|
|
203
|
+
}
|
|
204
|
+
const currentText = para.getTextContent();
|
|
205
|
+
const finalText = currentText.length > 0 && !currentText.endsWith(' ') ? ` ${insertText}` : insertText;
|
|
206
|
+
const newNode = $createTextNode(finalText);
|
|
207
|
+
para.append(newNode);
|
|
208
|
+
const newSel = $createRangeSelection();
|
|
209
|
+
const len = newNode.getTextContentSize();
|
|
210
|
+
newSel.anchor.set(newNode.getKey(), len, 'text');
|
|
211
|
+
newSel.focus.set(newNode.getKey(), len, 'text');
|
|
212
|
+
$setSelection(newSel);
|
|
213
|
+
}, // Tag facet selections so the update listener skips them — we handle
|
|
214
|
+
// state updates below to avoid the intermediate empty-items state.
|
|
215
|
+
isFacetSelection ? {
|
|
216
|
+
tag: 'shipql-apply'
|
|
217
|
+
} : undefined);
|
|
218
|
+
// For facet selections (e.g. "status" → inserts "status:"), notify the
|
|
219
|
+
// parent directly. The update listener is skipped (tagged) because it
|
|
220
|
+
// would call buildSuggestionItems with stale empty valueSuggestions,
|
|
221
|
+
// causing a flash of "No suggestions found".
|
|
222
|
+
if (isFacetSelection) {
|
|
223
|
+
setCurrentFacetRef.current(selectedValue);
|
|
224
|
+
let text = '';
|
|
225
|
+
editor.getEditorState().read(()=>{
|
|
226
|
+
text = $getRoot().getTextContent();
|
|
227
|
+
});
|
|
228
|
+
onLeafChangeRef.current?.({
|
|
229
|
+
partialValue: '',
|
|
230
|
+
ast: tryParse(text)
|
|
231
|
+
});
|
|
232
|
+
setOpenRef.current(true);
|
|
233
|
+
}
|
|
234
|
+
// Defer focus so it fires after the browser's blur event (which hasn't
|
|
235
|
+
// fired yet during the mousedown handler that calls apply). A synchronous
|
|
236
|
+
// focus() call here is a no-op because the editor is still focused.
|
|
237
|
+
setTimeout(()=>editor.focus(), 0);
|
|
238
|
+
hasNavigatedRef.current = false;
|
|
239
|
+
setSelectedIndexRef.current(-1);
|
|
240
|
+
};
|
|
241
|
+
return ()=>{
|
|
242
|
+
applyRef.current = null;
|
|
243
|
+
};
|
|
244
|
+
}, [
|
|
245
|
+
editor,
|
|
246
|
+
applyRef,
|
|
247
|
+
negationPrefixRef
|
|
248
|
+
]);
|
|
249
|
+
// ── Keyboard + update listeners ─────────────────────────────────────────────
|
|
250
|
+
useEffect(()=>{
|
|
251
|
+
const unregisterFocus = editor.registerCommand(FOCUS_COMMAND, ()=>{
|
|
252
|
+
isFocusedRef.current = true;
|
|
253
|
+
setOpenRef.current(true);
|
|
254
|
+
return false;
|
|
255
|
+
}, COMMAND_PRIORITY_LOW);
|
|
256
|
+
const unregisterBlur = editor.registerCommand(BLUR_COMMAND, ()=>{
|
|
257
|
+
isFocusedRef.current = false;
|
|
258
|
+
if (!isSelectingRef.current) {
|
|
259
|
+
setOpenRef.current(false);
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}, COMMAND_PRIORITY_LOW);
|
|
263
|
+
const unregisterArrowDown = editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (e)=>{
|
|
264
|
+
if (!openRef.current || itemsRef.current.length === 0) return false;
|
|
265
|
+
e?.preventDefault();
|
|
266
|
+
const its = itemsRef.current;
|
|
267
|
+
let next = selectedIndexRef.current + 1;
|
|
268
|
+
while(next < its.length && its[next]?.type === 'section-header')next++;
|
|
269
|
+
if (next >= its.length) {
|
|
270
|
+
next = 0;
|
|
271
|
+
while(next < its.length && its[next]?.type === 'section-header')next++;
|
|
272
|
+
}
|
|
273
|
+
if (next < its.length) {
|
|
274
|
+
hasNavigatedRef.current = true;
|
|
275
|
+
setSelectedIndexRef.current(next);
|
|
276
|
+
}
|
|
277
|
+
return true;
|
|
278
|
+
}, COMMAND_PRIORITY_NORMAL);
|
|
279
|
+
const unregisterArrowUp = editor.registerCommand(KEY_ARROW_UP_COMMAND, (e)=>{
|
|
280
|
+
if (!openRef.current || itemsRef.current.length === 0) return false;
|
|
281
|
+
e?.preventDefault();
|
|
282
|
+
const its = itemsRef.current;
|
|
283
|
+
let prev = selectedIndexRef.current - 1;
|
|
284
|
+
while(prev >= 0 && its[prev]?.type === 'section-header')prev--;
|
|
285
|
+
if (prev < 0) {
|
|
286
|
+
prev = its.length - 1;
|
|
287
|
+
while(prev >= 0 && its[prev]?.type === 'section-header')prev--;
|
|
288
|
+
}
|
|
289
|
+
if (prev >= 0) {
|
|
290
|
+
hasNavigatedRef.current = true;
|
|
291
|
+
setSelectedIndexRef.current(prev);
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}, COMMAND_PRIORITY_NORMAL);
|
|
295
|
+
const unregisterEnter = editor.registerCommand(KEY_ENTER_COMMAND, (e)=>{
|
|
296
|
+
if (!openRef.current || itemsRef.current.length === 0) return false;
|
|
297
|
+
if (!hasNavigatedRef.current || selectedIndexRef.current < 0) return false;
|
|
298
|
+
const item = itemsRef.current[selectedIndexRef.current];
|
|
299
|
+
if (!item || item.type === 'section-header') return false;
|
|
300
|
+
e?.preventDefault();
|
|
301
|
+
applyRef.current?.(item.value);
|
|
302
|
+
return true;
|
|
303
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
304
|
+
const unregisterTab = editor.registerCommand(KEY_TAB_COMMAND, (e)=>{
|
|
305
|
+
if (!openRef.current) return false;
|
|
306
|
+
e?.preventDefault();
|
|
307
|
+
editor.update(()=>{
|
|
308
|
+
const sel = $getSelection();
|
|
309
|
+
if (!$isRangeSelection(sel)) return;
|
|
310
|
+
const anchor = sel.anchor.getNode();
|
|
311
|
+
const para = $getRoot().getFirstChild();
|
|
312
|
+
if (!para) return;
|
|
313
|
+
if ($isShipQLLeafNode(anchor)) {
|
|
314
|
+
const next = anchor.getNextSibling();
|
|
315
|
+
const newSel = $createRangeSelection();
|
|
316
|
+
if (next && $isTextNode(next)) {
|
|
317
|
+
newSel.anchor.set(next.getKey(), 0, 'text');
|
|
318
|
+
newSel.focus.set(next.getKey(), 0, 'text');
|
|
319
|
+
} else {
|
|
320
|
+
const emptyNode = $createTextNode(' ');
|
|
321
|
+
para.append(emptyNode);
|
|
322
|
+
newSel.anchor.set(emptyNode.getKey(), 1, 'text');
|
|
323
|
+
newSel.focus.set(emptyNode.getKey(), 1, 'text');
|
|
324
|
+
}
|
|
325
|
+
$setSelection(newSel);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if ($isTextNode(anchor)) {
|
|
329
|
+
const len = anchor.getTextContentSize();
|
|
330
|
+
const newSel = $createRangeSelection();
|
|
331
|
+
newSel.anchor.set(anchor.getKey(), len, 'text');
|
|
332
|
+
newSel.focus.set(anchor.getKey(), len, 'text');
|
|
333
|
+
$setSelection(newSel);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
return true;
|
|
337
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
338
|
+
const unregisterEscape = editor.registerCommand(KEY_ESCAPE_COMMAND, (e)=>{
|
|
339
|
+
if (!openRef.current) return false;
|
|
340
|
+
e?.preventDefault();
|
|
341
|
+
setOpenRef.current(false);
|
|
342
|
+
return true;
|
|
343
|
+
}, COMMAND_PRIORITY_NORMAL);
|
|
344
|
+
const unregisterUpdate = editor.registerUpdateListener(({ editorState, tags })=>{
|
|
345
|
+
if (tags.has('shipql-rebuild') || tags.has('shipql-apply')) return;
|
|
346
|
+
editorState.read(()=>{
|
|
347
|
+
const para = $getRoot().getFirstChild();
|
|
348
|
+
if (!para) return;
|
|
349
|
+
let activeText = getActiveSegment(para);
|
|
350
|
+
let focusedLeaf = null;
|
|
351
|
+
const sel = $getSelection();
|
|
352
|
+
if ($isRangeSelection(sel)) {
|
|
353
|
+
const anchor = sel.anchor.getNode();
|
|
354
|
+
if ($isShipQLLeafNode(anchor)) {
|
|
355
|
+
const shipqlNode = anchor.getShipQLNode();
|
|
356
|
+
const textContent = anchor.getTextContent();
|
|
357
|
+
if (shipqlNode.type === 'text' || textContent !== shipqlNode.source) {
|
|
358
|
+
// Text-type leaf, or user is actively typing inside a leaf
|
|
359
|
+
// (text content diverged from the AST source). Use the live
|
|
360
|
+
// text so detectFacetContext can extract the partial value.
|
|
361
|
+
activeText = textContent;
|
|
362
|
+
focusedLeaf = null;
|
|
363
|
+
} else {
|
|
364
|
+
focusedLeaf = shipqlNode;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const facetCtx = detectFacetContext(activeText, normalizeFacets(facetsRef.current));
|
|
369
|
+
if (focusedLeaf) {
|
|
370
|
+
negationPrefixRef.current = focusedLeaf.type === 'not' ? negationPrefixFromSource(focusedLeaf.source) : '';
|
|
371
|
+
setCurrentFacetRef.current(extractFacetFromLeaf(focusedLeaf) ?? null);
|
|
372
|
+
} else {
|
|
373
|
+
negationPrefixRef.current = facetCtx?.negationPrefix ?? stripNegationPrefix(activeText).prefix;
|
|
374
|
+
setCurrentFacetRef.current(facetCtx?.facet ?? null);
|
|
375
|
+
}
|
|
376
|
+
const text = $getRoot().getTextContent();
|
|
377
|
+
onLeafChangeRef.current?.({
|
|
378
|
+
partialValue: facetCtx?.partialValue ?? '',
|
|
379
|
+
ast: tryParse(text)
|
|
380
|
+
});
|
|
381
|
+
const newItems = buildSuggestionItems(facetsRef.current, valueSuggestionsRef.current, activeText, focusedLeaf);
|
|
382
|
+
setItemsRef.current(newItems);
|
|
383
|
+
hasNavigatedRef.current = false;
|
|
384
|
+
setSelectedIndexRef.current(-1);
|
|
385
|
+
setOpenRef.current(isSelectingRef.current || isFocusedRef.current);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
return ()=>{
|
|
389
|
+
unregisterFocus();
|
|
390
|
+
unregisterBlur();
|
|
391
|
+
unregisterArrowDown();
|
|
392
|
+
unregisterArrowUp();
|
|
393
|
+
unregisterEnter();
|
|
394
|
+
unregisterTab();
|
|
395
|
+
unregisterEscape();
|
|
396
|
+
unregisterUpdate();
|
|
397
|
+
};
|
|
398
|
+
}, [
|
|
399
|
+
editor,
|
|
400
|
+
isSelectingRef,
|
|
401
|
+
applyRef,
|
|
402
|
+
negationPrefixRef
|
|
403
|
+
]);
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
//# sourceMappingURL=shipql-suggestions-plugin.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface RangeFacetConfig {
|
|
2
|
+
type: 'range';
|
|
3
|
+
min: string;
|
|
4
|
+
max: string;
|
|
5
|
+
presets?: string[];
|
|
6
|
+
}
|
|
7
|
+
export type FacetDef = string | {
|
|
8
|
+
name: string;
|
|
9
|
+
config: RangeFacetConfig;
|
|
10
|
+
};
|
|
11
|
+
export interface SuggestionItem {
|
|
12
|
+
value: string;
|
|
13
|
+
label: React.ReactNode;
|
|
14
|
+
icon: React.ReactNode | null;
|
|
15
|
+
selected: boolean;
|
|
16
|
+
type?: 'section-header' | 'range-slider';
|
|
17
|
+
rangeFacetConfig?: RangeFacetConfig;
|
|
18
|
+
facetName?: string;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -71,7 +71,7 @@ export function DataTable({ columns, data, pagination = true, pageSize = 10, pag
|
|
|
71
71
|
getCoreRowModel: getCoreRowModel(),
|
|
72
72
|
getFilteredRowModel: getFilteredRowModel(),
|
|
73
73
|
getSortedRowModel: getSortedRowModel(),
|
|
74
|
-
getPaginationRowModel:
|
|
74
|
+
getPaginationRowModel: pagination ? getPaginationRowModel() : undefined,
|
|
75
75
|
enableRowSelection: showSelectedCount,
|
|
76
76
|
onSortingChange: setSorting,
|
|
77
77
|
onColumnFiltersChange: setColumnFilters,
|
|
@@ -83,7 +83,7 @@ export function DataTable({ columns, data, pagination = true, pageSize = 10, pag
|
|
|
83
83
|
columnFilters,
|
|
84
84
|
columnVisibility,
|
|
85
85
|
rowSelection,
|
|
86
|
-
pagination:
|
|
86
|
+
pagination: pagination ? paginationState : undefined
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
const rowModel = table.getRowModel();
|