@shipfox/react-ui 0.28.0 → 0.30.0
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/dashboard/pages/analytics-page.js +9 -15
- package/dist/components/date-picker/date-picker.js +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/shipql-editor/index.d.ts +3 -0
- package/dist/components/shipql-editor/index.js +3 -0
- package/dist/components/shipql-editor/lexical/handle-arrow-right-from-leaf.d.ts +9 -0
- package/dist/components/shipql-editor/lexical/handle-arrow-right-from-leaf.js +46 -0
- package/dist/components/shipql-editor/lexical/leaf-close-overlay.d.ts +2 -0
- package/dist/components/shipql-editor/lexical/leaf-close-overlay.js +110 -0
- package/dist/components/shipql-editor/lexical/on-blur-plugin.d.ts +7 -0
- package/dist/components/shipql-editor/lexical/on-blur-plugin.js +27 -0
- package/dist/components/shipql-editor/lexical/on-text-change-plugin.d.ts +6 -0
- package/dist/components/shipql-editor/lexical/on-text-change-plugin.js +21 -0
- package/dist/components/shipql-editor/lexical/shipql-leaf-node.d.ts +31 -0
- package/dist/components/shipql-editor/lexical/shipql-leaf-node.js +124 -0
- package/dist/components/shipql-editor/lexical/shipql-plugin.d.ts +9 -0
- package/dist/components/shipql-editor/lexical/shipql-plugin.js +285 -0
- package/dist/components/shipql-editor/shipql-editor-inner.d.ts +3 -0
- package/dist/components/shipql-editor/shipql-editor-inner.js +103 -0
- package/dist/components/shipql-editor/shipql-editor.d.ts +21 -0
- package/dist/components/shipql-editor/shipql-editor.js +67 -0
- package/dist/components/table/table.stories.data.js +10 -5
- package/dist/styles.css +1 -1
- package/index.css +1051 -0
- package/package.json +10 -7
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
2
|
+
import { parse, removeBySource, stringify } from '@shipfox/shipql-parser';
|
|
3
|
+
import { $createRangeSelection, $createTextNode, $getRoot, $getSelection, $isRangeSelection, $isTextNode, $setSelection, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_NORMAL, createCommand, INSERT_PARAGRAPH_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ENTER_COMMAND } from 'lexical';
|
|
4
|
+
import { useEffect, useRef } from 'react';
|
|
5
|
+
import { handleArrowRightFromLeaf } from './handle-arrow-right-from-leaf.js';
|
|
6
|
+
import { $createShipQLLeafNode, $isShipQLLeafNode, isGroupedCompound, isSimpleLeaf, leafSource } from './shipql-leaf-node.js';
|
|
7
|
+
function collectLeaves(ast) {
|
|
8
|
+
// Simple terminals are always a single leaf chip.
|
|
9
|
+
if (isSimpleLeaf(ast)) {
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
source: leafSource(ast),
|
|
13
|
+
node: ast
|
|
14
|
+
}
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
// NOT: if the inner expression is a simple leaf or grouped compound,
|
|
18
|
+
// treat the whole NOT as a single chip. Otherwise recurse into expr.
|
|
19
|
+
if (ast.type === 'not') {
|
|
20
|
+
if (isSimpleLeaf(ast.expr) || isGroupedCompound(ast.expr)) {
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
source: ast.source,
|
|
24
|
+
node: ast
|
|
25
|
+
}
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
return collectLeaves(ast.expr);
|
|
29
|
+
}
|
|
30
|
+
// AND/OR: if the node is a grouped compound like `env:(prod OR staging)`,
|
|
31
|
+
// treat the whole thing as a single chip. Otherwise recurse both sides.
|
|
32
|
+
if (ast.type === 'and' || ast.type === 'or') {
|
|
33
|
+
if (isGroupedCompound(ast)) {
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
source: ast.source,
|
|
37
|
+
node: ast
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
return [
|
|
42
|
+
...collectLeaves(ast.left),
|
|
43
|
+
...collectLeaves(ast.right)
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
function tokenize(fullText, leaves) {
|
|
49
|
+
const segments = [];
|
|
50
|
+
let remaining = fullText;
|
|
51
|
+
for (const { source, node } of leaves){
|
|
52
|
+
const idx = remaining.indexOf(source);
|
|
53
|
+
if (idx === -1) {
|
|
54
|
+
// Source not found in remaining text — bail and treat all as operator
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
kind: 'op',
|
|
58
|
+
text: fullText
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
if (idx > 0) segments.push({
|
|
63
|
+
kind: 'op',
|
|
64
|
+
text: remaining.slice(0, idx)
|
|
65
|
+
});
|
|
66
|
+
segments.push({
|
|
67
|
+
kind: 'leaf',
|
|
68
|
+
text: source,
|
|
69
|
+
node
|
|
70
|
+
});
|
|
71
|
+
remaining = remaining.slice(idx + source.length);
|
|
72
|
+
}
|
|
73
|
+
if (remaining.length > 0) segments.push({
|
|
74
|
+
kind: 'op',
|
|
75
|
+
text: remaining
|
|
76
|
+
});
|
|
77
|
+
return segments.filter((s)=>s.text.length > 0);
|
|
78
|
+
}
|
|
79
|
+
function needsRebuild(children, segments) {
|
|
80
|
+
if (children.length !== segments.length) return true;
|
|
81
|
+
for(let i = 0; i < children.length; i++){
|
|
82
|
+
const child = children[i];
|
|
83
|
+
const seg = segments[i];
|
|
84
|
+
if ($isShipQLLeafNode(child) !== (seg.kind === 'leaf')) return true;
|
|
85
|
+
if ($isTextNode(child) && child.getTextContent() !== seg.text) return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
function getAbsoluteOffset(para, point) {
|
|
90
|
+
let offset = 0;
|
|
91
|
+
for (const child of para.getChildren()){
|
|
92
|
+
if (child.getKey() === point.key) return offset + point.offset;
|
|
93
|
+
if ($isTextNode(child)) offset += child.getTextContentSize();
|
|
94
|
+
}
|
|
95
|
+
return offset + point.offset;
|
|
96
|
+
}
|
|
97
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
98
|
+
/** Payload: the Lexical node key of the leaf to remove. */ export const REMOVE_LEAF_COMMAND = createCommand('REMOVE_LEAF_COMMAND');
|
|
99
|
+
// ─── Plugin ───────────────────────────────────────────────────────────────────
|
|
100
|
+
const REBUILD_TAG = 'shipql-rebuild';
|
|
101
|
+
export function ShipQLPlugin({ onLeafFocus }) {
|
|
102
|
+
const [editor] = useLexicalComposerContext();
|
|
103
|
+
// Keep latest callback accessible inside Lexical listeners without re-registering.
|
|
104
|
+
const onLeafFocusRef = useRef(onLeafFocus);
|
|
105
|
+
onLeafFocusRef.current = onLeafFocus;
|
|
106
|
+
// Track the key of the last focused leaf to avoid redundant callbacks.
|
|
107
|
+
const lastFocusedKeyRef = useRef(null);
|
|
108
|
+
useEffect(()=>{
|
|
109
|
+
// Block newlines — ShipQL is a single-line language.
|
|
110
|
+
const unregisterEnter = editor.registerCommand(KEY_ENTER_COMMAND, (event)=>{
|
|
111
|
+
event?.preventDefault();
|
|
112
|
+
return true;
|
|
113
|
+
}, COMMAND_PRIORITY_HIGH);
|
|
114
|
+
const unregisterParagraph = editor.registerCommand(INSERT_PARAGRAPH_COMMAND, ()=>true, COMMAND_PRIORITY_HIGH);
|
|
115
|
+
const unregisterArrowRight = editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, (event)=>handleArrowRightFromLeaf(event, editor), COMMAND_PRIORITY_NORMAL);
|
|
116
|
+
const unregisterRemoveLeaf = editor.registerCommand(REMOVE_LEAF_COMMAND, (nodeKey)=>{
|
|
117
|
+
editor.update(()=>{
|
|
118
|
+
const para = $getRoot().getFirstChild();
|
|
119
|
+
if (!para) return;
|
|
120
|
+
// Find the leaf node's source text from its key.
|
|
121
|
+
let targetSource = null;
|
|
122
|
+
for (const child of para.getChildren()){
|
|
123
|
+
if ($isShipQLLeafNode(child) && child.getKey() === nodeKey) {
|
|
124
|
+
targetSource = child.getTextContent();
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!targetSource) return;
|
|
129
|
+
// Parse, remove, re-stringify.
|
|
130
|
+
const fullText = para.getTextContent();
|
|
131
|
+
let ast = null;
|
|
132
|
+
try {
|
|
133
|
+
ast = parse(fullText);
|
|
134
|
+
} catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!ast) return;
|
|
138
|
+
const newAst = removeBySource(ast, targetSource);
|
|
139
|
+
const newText = stringify(newAst);
|
|
140
|
+
// Replace paragraph content — the update listener will re-tokenize.
|
|
141
|
+
para.clear();
|
|
142
|
+
if (newText) {
|
|
143
|
+
para.append($createTextNode(newText));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return true;
|
|
147
|
+
}, COMMAND_PRIORITY_HIGH);
|
|
148
|
+
const unregisterUpdate = editor.registerUpdateListener(({ editorState, tags })=>{
|
|
149
|
+
// After our own rebuild we only need to refresh the leaf-focus callback.
|
|
150
|
+
if (tags.has(REBUILD_TAG)) {
|
|
151
|
+
editorState.read(()=>{
|
|
152
|
+
const sel = $getSelection();
|
|
153
|
+
if (!$isRangeSelection(sel)) return;
|
|
154
|
+
const anchor = sel.anchor.getNode();
|
|
155
|
+
const key = $isShipQLLeafNode(anchor) ? anchor.getKey() : null;
|
|
156
|
+
if (key !== lastFocusedKeyRef.current) {
|
|
157
|
+
lastFocusedKeyRef.current = key;
|
|
158
|
+
onLeafFocusRef.current?.(key !== null && $isShipQLLeafNode(anchor) ? anchor.getShipQLNode() : null);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// ── Read current editor state ──────────────────────────────────────────
|
|
164
|
+
let shouldRebuild = false;
|
|
165
|
+
let nextSegments = [];
|
|
166
|
+
let savedOffset = -1;
|
|
167
|
+
editorState.read(()=>{
|
|
168
|
+
const para = $getRoot().getFirstChild();
|
|
169
|
+
if (!para) return;
|
|
170
|
+
const text = para.getTextContent();
|
|
171
|
+
const children = para.getChildren();
|
|
172
|
+
let ast = null;
|
|
173
|
+
try {
|
|
174
|
+
ast = parse(text);
|
|
175
|
+
} catch {
|
|
176
|
+
// parse error — fall through with ast = null
|
|
177
|
+
}
|
|
178
|
+
if (!ast) {
|
|
179
|
+
// Clear all leaf highlights if any exist.
|
|
180
|
+
if (children.some($isShipQLLeafNode)) {
|
|
181
|
+
shouldRebuild = true;
|
|
182
|
+
nextSegments = text ? [
|
|
183
|
+
{
|
|
184
|
+
kind: 'op',
|
|
185
|
+
text
|
|
186
|
+
}
|
|
187
|
+
] : [];
|
|
188
|
+
const sel = $getSelection();
|
|
189
|
+
if ($isRangeSelection(sel) && sel.anchor.type === 'text') {
|
|
190
|
+
savedOffset = getAbsoluteOffset(para, sel.anchor);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Cursor left any leaf.
|
|
194
|
+
if (lastFocusedKeyRef.current !== null) {
|
|
195
|
+
lastFocusedKeyRef.current = null;
|
|
196
|
+
onLeafFocusRef.current?.(null);
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const segments = tokenize(text, collectLeaves(ast));
|
|
201
|
+
if (needsRebuild(children, segments)) {
|
|
202
|
+
shouldRebuild = true;
|
|
203
|
+
nextSegments = segments;
|
|
204
|
+
const sel = $getSelection();
|
|
205
|
+
if ($isRangeSelection(sel) && sel.anchor.type === 'text') {
|
|
206
|
+
savedOffset = getAbsoluteOffset(para, sel.anchor);
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
// No structural change — just update leaf-focus.
|
|
210
|
+
const sel = $getSelection();
|
|
211
|
+
if ($isRangeSelection(sel)) {
|
|
212
|
+
const anchor = sel.anchor.getNode();
|
|
213
|
+
const key = $isShipQLLeafNode(anchor) ? anchor.getKey() : null;
|
|
214
|
+
if (key !== lastFocusedKeyRef.current) {
|
|
215
|
+
lastFocusedKeyRef.current = key;
|
|
216
|
+
onLeafFocusRef.current?.(key !== null && $isShipQLLeafNode(anchor) ? anchor.getShipQLNode() : null);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
if (!shouldRebuild) return;
|
|
222
|
+
// ── Rebuild node structure ─────────────────────────────────────────────
|
|
223
|
+
editor.update(()=>{
|
|
224
|
+
const para = $getRoot().getFirstChild();
|
|
225
|
+
if (!para) return;
|
|
226
|
+
para.clear();
|
|
227
|
+
const newNodes = nextSegments.map((seg)=>seg.kind === 'leaf' ? $createShipQLLeafNode(seg.text, seg.node) : $createTextNode(seg.text));
|
|
228
|
+
for (const node of newNodes)para.append(node);
|
|
229
|
+
// Restore cursor to the same absolute character position.
|
|
230
|
+
if (savedOffset >= 0 && newNodes.length > 0) {
|
|
231
|
+
let remaining = savedOffset;
|
|
232
|
+
for (const node of newNodes){
|
|
233
|
+
const len = $isTextNode(node) ? node.getTextContentSize() : 0;
|
|
234
|
+
if (remaining <= len) {
|
|
235
|
+
const sel = $createRangeSelection();
|
|
236
|
+
sel.anchor.set(node.getKey(), remaining, 'text');
|
|
237
|
+
sel.focus.set(node.getKey(), remaining, 'text');
|
|
238
|
+
$setSelection(sel);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
remaining -= len;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}, {
|
|
245
|
+
tag: REBUILD_TAG
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
// Perform the initial tokenization pass directly. The update listener is
|
|
249
|
+
// registered after Lexical has already committed its initialConfig editorState
|
|
250
|
+
// (useEffect runs post-paint), so the listener never fires for the first render.
|
|
251
|
+
// We do the full rebuild here instead of relying on the listener being triggered.
|
|
252
|
+
editor.update(()=>{
|
|
253
|
+
const para = $getRoot().getFirstChild();
|
|
254
|
+
if (!para) return;
|
|
255
|
+
const text = para.getTextContent();
|
|
256
|
+
if (!text) return;
|
|
257
|
+
const children = para.getChildren();
|
|
258
|
+
let ast = null;
|
|
259
|
+
try {
|
|
260
|
+
ast = parse(text);
|
|
261
|
+
} catch {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!ast) return;
|
|
265
|
+
const segments = tokenize(text, collectLeaves(ast));
|
|
266
|
+
if (!needsRebuild(children, segments)) return;
|
|
267
|
+
para.clear();
|
|
268
|
+
for (const seg of segments){
|
|
269
|
+
para.append(seg.kind === 'leaf' ? $createShipQLLeafNode(seg.text, seg.node) : $createTextNode(seg.text));
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
return ()=>{
|
|
273
|
+
unregisterEnter();
|
|
274
|
+
unregisterParagraph();
|
|
275
|
+
unregisterArrowRight();
|
|
276
|
+
unregisterRemoveLeaf();
|
|
277
|
+
unregisterUpdate();
|
|
278
|
+
};
|
|
279
|
+
}, [
|
|
280
|
+
editor
|
|
281
|
+
]);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
//# sourceMappingURL=shipql-plugin.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ShipQLEditorInnerProps } from './shipql-editor';
|
|
2
|
+
export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder, className, disabled, mode, text, editorKey, isError, onTextChange, onClear, onToggleMode, }: ShipQLEditorInnerProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
//# sourceMappingURL=shipql-editor-inner.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
3
|
+
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
4
|
+
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
5
|
+
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|
6
|
+
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
|
|
7
|
+
import { Input } from '../../components/input/index.js';
|
|
8
|
+
import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical';
|
|
9
|
+
import { cn } from '../../utils/cn.js';
|
|
10
|
+
import { Icon } from '../icon/icon.js';
|
|
11
|
+
import { LeafCloseOverlay } from './lexical/leaf-close-overlay.js';
|
|
12
|
+
import { OnBlurPlugin } from './lexical/on-blur-plugin.js';
|
|
13
|
+
import { OnTextChangePlugin } from './lexical/on-text-change-plugin.js';
|
|
14
|
+
import { ShipQLLeafNode } from './lexical/shipql-leaf-node.js';
|
|
15
|
+
import { ShipQLPlugin } from './lexical/shipql-plugin.js';
|
|
16
|
+
const INPUT_CLASSES = 'block w-full rounded-6 bg-background-field-base py-2 pl-7 pr-58 sm:pr-64 text-md text-foreground-neutral-base caret-foreground-neutral-base outline-none focus:border-border-highlights-interactive shadow-button-neutral';
|
|
17
|
+
const INPUT_ERROR_CLASSES = 'shadow-border-error';
|
|
18
|
+
const BUTTON_CLASSES = 'shrink-0 text-foreground-neutral-subtle hover:text-foreground-neutral-base transition-all duration-150 flex justify-center items-center cursor-pointer w-28 sm:w-32 h-full';
|
|
19
|
+
export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder, className, disabled, mode, text, editorKey, isError, onTextChange, onClear, onToggleMode }) {
|
|
20
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
21
|
+
"data-shipql-editor": true,
|
|
22
|
+
className: cn('relative', className),
|
|
23
|
+
children: [
|
|
24
|
+
mode === 'editor' ? /*#__PURE__*/ _jsxs(LexicalComposer, {
|
|
25
|
+
initialConfig: {
|
|
26
|
+
namespace: 'ShipQLEditor',
|
|
27
|
+
nodes: [
|
|
28
|
+
ShipQLLeafNode
|
|
29
|
+
],
|
|
30
|
+
onError: (error)=>{
|
|
31
|
+
throw error;
|
|
32
|
+
},
|
|
33
|
+
editorState: text ? ()=>{
|
|
34
|
+
const para = $createParagraphNode();
|
|
35
|
+
para.append($createTextNode(text));
|
|
36
|
+
$getRoot().append(para);
|
|
37
|
+
} : undefined
|
|
38
|
+
},
|
|
39
|
+
children: [
|
|
40
|
+
/*#__PURE__*/ _jsx(PlainTextPlugin, {
|
|
41
|
+
contentEditable: /*#__PURE__*/ _jsx(ContentEditable, {
|
|
42
|
+
id: "shipql-editor",
|
|
43
|
+
"aria-label": "ShipQL query editor",
|
|
44
|
+
className: cn(INPUT_CLASSES, isError && INPUT_ERROR_CLASSES, disabled && 'pointer-events-none opacity-50')
|
|
45
|
+
}),
|
|
46
|
+
placeholder: placeholder ? /*#__PURE__*/ _jsx("div", {
|
|
47
|
+
className: "pointer-events-none absolute left-0 top-0 select-none px-7 py-2 text-md text-foreground-neutral-muted",
|
|
48
|
+
children: placeholder
|
|
49
|
+
}) : null,
|
|
50
|
+
ErrorBoundary: LexicalErrorBoundary
|
|
51
|
+
}),
|
|
52
|
+
/*#__PURE__*/ _jsx(ShipQLPlugin, {
|
|
53
|
+
onLeafFocus: onLeafFocus
|
|
54
|
+
}),
|
|
55
|
+
/*#__PURE__*/ _jsx(OnBlurPlugin, {
|
|
56
|
+
onChange: onChange
|
|
57
|
+
}),
|
|
58
|
+
/*#__PURE__*/ _jsx(OnTextChangePlugin, {
|
|
59
|
+
onTextChange: onTextChange
|
|
60
|
+
}),
|
|
61
|
+
/*#__PURE__*/ _jsx(HistoryPlugin, {}),
|
|
62
|
+
!disabled && /*#__PURE__*/ _jsx(LeafCloseOverlay, {})
|
|
63
|
+
]
|
|
64
|
+
}, editorKey) : /*#__PURE__*/ _jsx(Input, {
|
|
65
|
+
ref: (el)=>el?.focus(),
|
|
66
|
+
"aria-label": "ShipQL query editor",
|
|
67
|
+
className: cn(INPUT_CLASSES, disabled && 'pointer-events-none opacity-50'),
|
|
68
|
+
"aria-invalid": isError,
|
|
69
|
+
value: text,
|
|
70
|
+
onChange: (e)=>onTextChange(e.target.value),
|
|
71
|
+
placeholder: placeholder,
|
|
72
|
+
disabled: disabled
|
|
73
|
+
}),
|
|
74
|
+
!disabled && /*#__PURE__*/ _jsxs("div", {
|
|
75
|
+
className: "absolute right-0 top-0 flex h-28 items-center",
|
|
76
|
+
children: [
|
|
77
|
+
/*#__PURE__*/ _jsx("button", {
|
|
78
|
+
type: "button",
|
|
79
|
+
"aria-label": "Clear query",
|
|
80
|
+
className: cn(BUTTON_CLASSES, text.trim().length > 0 ? 'opacity-100' : 'opacity-0 pointer-events-none'),
|
|
81
|
+
onClick: onClear,
|
|
82
|
+
children: /*#__PURE__*/ _jsx(Icon, {
|
|
83
|
+
name: "closeLine",
|
|
84
|
+
size: 16
|
|
85
|
+
})
|
|
86
|
+
}),
|
|
87
|
+
/*#__PURE__*/ _jsx("button", {
|
|
88
|
+
type: "button",
|
|
89
|
+
"aria-label": mode === 'editor' ? 'Switch to free text mode' : 'Switch to editor mode',
|
|
90
|
+
className: cn(BUTTON_CLASSES, 'sm:border-l border-border-neutral-strong'),
|
|
91
|
+
onClick: onToggleMode,
|
|
92
|
+
children: /*#__PURE__*/ _jsx(Icon, {
|
|
93
|
+
name: mode === 'editor' ? 'edit2Line' : 'codeLine',
|
|
94
|
+
size: 16
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
]
|
|
98
|
+
})
|
|
99
|
+
]
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=shipql-editor-inner.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AstNode } from '@shipfox/shipql-parser';
|
|
2
|
+
import type { LeafAstNode } from './lexical/shipql-leaf-node';
|
|
3
|
+
export interface ShipQLEditorProps {
|
|
4
|
+
defaultValue?: string;
|
|
5
|
+
onChange?: (ast: AstNode) => void;
|
|
6
|
+
onLeafFocus?: (node: LeafAstNode | null) => void;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ShipQLEditorInnerProps extends ShipQLEditorProps {
|
|
12
|
+
mode: 'editor' | 'text';
|
|
13
|
+
text: string;
|
|
14
|
+
editorKey: number;
|
|
15
|
+
isError: boolean;
|
|
16
|
+
onTextChange: (text: string) => void;
|
|
17
|
+
onClear: () => void;
|
|
18
|
+
onToggleMode: () => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function ShipQLEditor({ disabled, className, ...props }: ShipQLEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
//# sourceMappingURL=shipql-editor.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { parse } from '@shipfox/shipql-parser';
|
|
3
|
+
import { lazy, Suspense, useCallback, useRef, useState } from 'react';
|
|
4
|
+
import { cn } from '../../utils/cn.js';
|
|
5
|
+
function isParseError(text) {
|
|
6
|
+
if (!text.trim()) return false;
|
|
7
|
+
try {
|
|
8
|
+
return parse(text) === null;
|
|
9
|
+
} catch {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const ShipQLEditorInner = /*#__PURE__*/ lazy(()=>import('./shipql-editor-inner.js'));
|
|
14
|
+
export function ShipQLEditor({ disabled, className, ...props }) {
|
|
15
|
+
const [mode, setMode] = useState('editor');
|
|
16
|
+
const [text, setText] = useState(props.defaultValue ?? '');
|
|
17
|
+
const [editorKey, setEditorKey] = useState(0);
|
|
18
|
+
const [isError, setIsError] = useState(()=>isParseError(props.defaultValue ?? ''));
|
|
19
|
+
const textRef = useRef(text);
|
|
20
|
+
textRef.current = text;
|
|
21
|
+
const clearingRef = useRef(false);
|
|
22
|
+
const handleTextChange = useCallback((newText)=>{
|
|
23
|
+
if (clearingRef.current) {
|
|
24
|
+
if (newText.trim() === '') clearingRef.current = false;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
textRef.current = newText;
|
|
28
|
+
setText(newText);
|
|
29
|
+
setIsError(isParseError(newText));
|
|
30
|
+
}, []);
|
|
31
|
+
const handleClear = useCallback(()=>{
|
|
32
|
+
clearingRef.current = true;
|
|
33
|
+
setText('');
|
|
34
|
+
textRef.current = '';
|
|
35
|
+
setIsError(false);
|
|
36
|
+
setEditorKey((k)=>k + 1);
|
|
37
|
+
}, []);
|
|
38
|
+
const handleToggleMode = useCallback(()=>{
|
|
39
|
+
setMode((m)=>{
|
|
40
|
+
if (m === 'editor') {
|
|
41
|
+
setText(textRef.current);
|
|
42
|
+
return 'text';
|
|
43
|
+
}
|
|
44
|
+
setEditorKey((k)=>k + 1);
|
|
45
|
+
return 'editor';
|
|
46
|
+
});
|
|
47
|
+
}, []);
|
|
48
|
+
return /*#__PURE__*/ _jsx(Suspense, {
|
|
49
|
+
fallback: /*#__PURE__*/ _jsx("div", {
|
|
50
|
+
className: cn('h-8 w-full animate-pulse rounded-6 bg-background-components-base', className)
|
|
51
|
+
}),
|
|
52
|
+
children: /*#__PURE__*/ _jsx(ShipQLEditorInner, {
|
|
53
|
+
...props,
|
|
54
|
+
className: className,
|
|
55
|
+
disabled: disabled,
|
|
56
|
+
mode: mode,
|
|
57
|
+
text: text,
|
|
58
|
+
editorKey: editorKey,
|
|
59
|
+
isError: isError,
|
|
60
|
+
onTextChange: handleTextChange,
|
|
61
|
+
onClear: handleClear,
|
|
62
|
+
onToggleMode: handleToggleMode
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=shipql-editor.js.map
|
|
@@ -62,16 +62,21 @@
|
|
|
62
62
|
];
|
|
63
63
|
return Array.from({
|
|
64
64
|
length: count
|
|
65
|
-
}, (_, i)=>
|
|
65
|
+
}, (_, i)=>{
|
|
66
|
+
const total = 40 + (i * 7 + 3) % 80;
|
|
67
|
+
const failed = (i * 3 + 1) % 20;
|
|
68
|
+
const success = total - failed;
|
|
69
|
+
return {
|
|
66
70
|
id: `job-${i + 1}`,
|
|
67
71
|
name: jobNames[i % jobNames.length],
|
|
68
|
-
total
|
|
69
|
-
success
|
|
70
|
-
failed
|
|
72
|
+
total,
|
|
73
|
+
success,
|
|
74
|
+
failed,
|
|
71
75
|
status: statuses[i % statuses.length],
|
|
72
76
|
repository: repositories[i % repositories.length],
|
|
73
77
|
branch: branches[i % branches.length]
|
|
74
|
-
}
|
|
78
|
+
};
|
|
79
|
+
});
|
|
75
80
|
};
|
|
76
81
|
/**
|
|
77
82
|
* Sample job data
|