@dataloop-ai/components 0.20.127 → 0.20.128
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/package.json +2 -1
- package/src/components/compound/DlSearches/DlSmartSearch/components/DlSmartSearchInput.vue +44 -48
- package/src/components/compound/DlSearches/DlSmartSearch/utils/highlightSyntax.ts +2 -1
- package/src/hooks/use-suggestions.ts +58 -8
- package/src/utils/splitByQuotes.ts +109 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dataloop-ai/components",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.128",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": "./index.ts",
|
|
6
6
|
"./models": "./models.ts",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"sass": "^1.51.0",
|
|
38
38
|
"sass-loader": "^12.6.0",
|
|
39
39
|
"sortablejs": "^1.15.0",
|
|
40
|
+
"tokenizr": "^1.7.0",
|
|
40
41
|
"uuid": "^8.3.2",
|
|
41
42
|
"v-wave": "^1.5.0",
|
|
42
43
|
"vanilla-jsoneditor": "^0.10.2",
|
|
@@ -181,6 +181,7 @@ import {
|
|
|
181
181
|
} from '../../../../../hooks/use-suggestions'
|
|
182
182
|
import { parseSmartQuery, stringifySmartQuery } from '../../../../../utils'
|
|
183
183
|
import { StateManager, stateManager } from '../../../../../StateManager'
|
|
184
|
+
import { TokenType, tokenize } from '../../../../../utils/splitByQuotes'
|
|
184
185
|
|
|
185
186
|
export default defineComponent({
|
|
186
187
|
components: {
|
|
@@ -428,68 +429,63 @@ export default defineComponent({
|
|
|
428
429
|
const value = '' + suggestion
|
|
429
430
|
let stringValue = ''
|
|
430
431
|
let caretPosition = 0
|
|
431
|
-
if (searchQuery.value.length) {
|
|
432
|
-
let queryLeftSide = searchQuery.value.substring(
|
|
433
|
-
0,
|
|
434
|
-
caretAt.value
|
|
435
|
-
)
|
|
436
|
-
let queryRightSide = searchQuery.value.substring(caretAt.value)
|
|
437
432
|
|
|
433
|
+
const search = searchQuery.value ?? ''
|
|
434
|
+
const tokens = tokenize(search)
|
|
435
|
+
let leftTokenIndex = tokens.length
|
|
436
|
+
while(leftTokenIndex-- > 0) {
|
|
437
|
+
if (tokens[leftTokenIndex].pos < caretAt.value) {
|
|
438
|
+
break
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (leftTokenIndex < 0) {
|
|
443
|
+
stringValue = value + ' ' + search.replace(/^\S*\s*/, '')
|
|
444
|
+
caretPosition = value.length + 1
|
|
445
|
+
} else {
|
|
446
|
+
const token = tokens[leftTokenIndex]
|
|
447
|
+
const tokenLeftText = token.text.substring(0, caretAt.value - token.pos)
|
|
448
|
+
const tokenRightText = token.text.substring(caretAt.value - token.pos)
|
|
449
|
+
|
|
450
|
+
if (token.type === TokenType.WHITESPACE) {
|
|
451
|
+
// caret after space
|
|
452
|
+
token.text = ' ' + value + ' '
|
|
453
|
+
caretPosition = token.pos + 1 + value.length + 1
|
|
454
|
+
} else
|
|
438
455
|
if (['AND', 'OR'].includes(value)) {
|
|
439
456
|
// do not replace text if the value is AND or OR
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
queryLeftSide.replace(/\S+$/, '').trimEnd() + ' '
|
|
443
|
-
queryRightSide = leftover + queryRightSide
|
|
457
|
+
token.text = value + ' ' + token.text
|
|
458
|
+
caretPosition = token.pos + value.length + 1
|
|
444
459
|
} else if (value.startsWith('.')) {
|
|
445
460
|
// dot notation case
|
|
446
|
-
const words =
|
|
461
|
+
const words = tokenLeftText.split('.')
|
|
447
462
|
const lastWord = words.pop()
|
|
448
463
|
if (!value.startsWith('.' + lastWord)) {
|
|
449
464
|
words.push(lastWord)
|
|
450
465
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
// caret after space: only replace multiple spaces on the left
|
|
457
|
-
queryLeftSide = queryLeftSide.trimEnd() + ' '
|
|
458
|
-
} else if (/\.\S+$/.test(queryLeftSide)) {
|
|
459
|
-
// if there are dots in left side expression, suggestions have an operator
|
|
466
|
+
const text = words.join('.')
|
|
467
|
+
token.text = text + value + ' ' + tokenRightText
|
|
468
|
+
caretPosition = token.pos + text.length + value.length + 1
|
|
469
|
+
} else if (/\.\S+$/.test(tokenLeftText)) {
|
|
470
|
+
// if there are dots in left side expression...
|
|
460
471
|
// looks like a bug in findSuggestions TODO find it - for now work around it here
|
|
461
|
-
const leftover =
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
.
|
|
466
|
-
} else if (queryRightSide.startsWith(' ')) {
|
|
467
|
-
// this| situation: replace whatever is there on the left side with the value
|
|
468
|
-
queryLeftSide = queryLeftSide.replace(/\S+$/, '')
|
|
469
|
-
queryRightSide = queryRightSide.trimStart()
|
|
472
|
+
const leftover = tokenRightText.match(/^\S+/)?.[0] || ''
|
|
473
|
+
token.text = tokenLeftText + leftover + ' ' + value + ' ' +
|
|
474
|
+
tokenRightText.substring(leftover.length).trimStart()
|
|
475
|
+
caretPosition = token.pos + tokenLeftText.length +
|
|
476
|
+
leftover.length + 1 + value.length + 1
|
|
470
477
|
} else {
|
|
478
|
+
// this| situation: replace whatever is there on the left side with the value
|
|
471
479
|
// this|situation: replace whatever is there on both sides with the value
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
/(?<!\\)'([^']+\\')*[^']*((?<!\\)')?$/,
|
|
479
|
-
''
|
|
480
|
-
)
|
|
481
|
-
} else {
|
|
482
|
-
queryLeftSide = queryLeftSide.replace(/[^'\s]+$/, '')
|
|
480
|
+
const newValue = token.type === TokenType.COMMA ? ', ' + value : value
|
|
481
|
+
token.text = newValue
|
|
482
|
+
caretPosition = token.pos + newValue.length
|
|
483
|
+
if (tokens[leftTokenIndex + 1]?.type !== TokenType.WHITESPACE) {
|
|
484
|
+
token.text += ' '
|
|
485
|
+
caretPosition += 1
|
|
483
486
|
}
|
|
484
|
-
queryRightSide =
|
|
485
|
-
removeLeadingExpression(queryRightSide).trimStart()
|
|
486
487
|
}
|
|
487
|
-
|
|
488
|
-
stringValue = queryLeftSide + value + ' ' + queryRightSide
|
|
489
|
-
caretPosition = stringValue.length - queryRightSide.length
|
|
490
|
-
} else {
|
|
491
|
-
stringValue = value + ' '
|
|
492
|
-
caretPosition = stringValue.length
|
|
488
|
+
stringValue = tokens.map(token => token.text).join('')
|
|
493
489
|
}
|
|
494
490
|
|
|
495
491
|
setInputValue(stringValue)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { tokenize } from '../../../../../utils/splitByQuotes'
|
|
1
2
|
import { SyntaxColorSchema } from '../types'
|
|
2
3
|
|
|
3
4
|
const SPAN_STYLES = `overflow: hidden;
|
|
@@ -91,7 +92,7 @@ function restoreSelection(
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function renderText(text: string, colorSchema: SyntaxColorSchema) {
|
|
94
|
-
const words = text
|
|
95
|
+
const words = tokenize(text ?? '').map(token => token.text)
|
|
95
96
|
const output = words?.map((word) => {
|
|
96
97
|
if (colorSchema) {
|
|
97
98
|
if (colorSchema.keywords.values.includes(word)) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Ref, ref } from 'vue-demi'
|
|
2
|
-
import { splitByQuotes } from '../utils/splitByQuotes'
|
|
2
|
+
import { splitByQuotes, tokenize, TokenType } from '../utils/splitByQuotes'
|
|
3
|
+
import { Token } from 'tokenizr'
|
|
3
4
|
import { flatten } from 'flat'
|
|
4
5
|
import { isObject } from 'lodash'
|
|
5
6
|
|
|
@@ -192,12 +193,56 @@ export const useSuggestions = (
|
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
const findSuggestions = (input: string) => {
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
const tokens = tokenize(input)
|
|
197
|
+
|
|
198
|
+
let fieldToken: Token | null = null
|
|
199
|
+
let operatorToken: Token | null = null
|
|
200
|
+
let valueToken: Token | null = null
|
|
201
|
+
let keywordToken: Token | null = null
|
|
202
|
+
|
|
203
|
+
let i = tokens.length -1
|
|
204
|
+
let whitespace = false
|
|
205
|
+
while (i > -1) {
|
|
206
|
+
const token = tokens[i]
|
|
207
|
+
switch (token.type) {
|
|
208
|
+
case TokenType.WHITESPACE:
|
|
209
|
+
whitespace = true
|
|
210
|
+
break
|
|
211
|
+
case TokenType.LOGICAL:
|
|
212
|
+
keywordToken = token
|
|
213
|
+
break
|
|
214
|
+
case TokenType.BOOLEAN:
|
|
215
|
+
case TokenType.COMMA:
|
|
216
|
+
case TokenType.DATETIME:
|
|
217
|
+
case TokenType.NUMBER:
|
|
218
|
+
case TokenType.STRING:
|
|
219
|
+
case TokenType.PARTIAL_VALUE:
|
|
220
|
+
if (!valueToken) {
|
|
221
|
+
valueToken = token
|
|
222
|
+
}
|
|
223
|
+
break
|
|
224
|
+
case TokenType.OPERATOR:
|
|
225
|
+
// quirk: mapWordsToExpression would only set operator if followed by a whitespace
|
|
226
|
+
if (whitespace || valueToken) {
|
|
227
|
+
operatorToken = token
|
|
228
|
+
}
|
|
229
|
+
break
|
|
230
|
+
case TokenType.FIELD:
|
|
231
|
+
fieldToken = token
|
|
232
|
+
i = 0
|
|
233
|
+
break
|
|
234
|
+
}
|
|
235
|
+
i--
|
|
236
|
+
}
|
|
197
237
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
238
|
+
const expressions: Expression[] = [{
|
|
239
|
+
field: fieldToken?.text,
|
|
240
|
+
operator: operatorToken?.text,
|
|
241
|
+
value: valueToken?.type === TokenType.COMMA ? '' : valueToken?.text,
|
|
242
|
+
keyword: keywordToken?.text
|
|
243
|
+
}]
|
|
244
|
+
|
|
245
|
+
localSuggestions = sortedSuggestions
|
|
201
246
|
|
|
202
247
|
for (const { field, operator, value, keyword } of expressions) {
|
|
203
248
|
let matchedField: Suggestion | null = null
|
|
@@ -228,7 +273,9 @@ export const useSuggestions = (
|
|
|
228
273
|
|
|
229
274
|
if (
|
|
230
275
|
!matchedField ||
|
|
231
|
-
(
|
|
276
|
+
(
|
|
277
|
+
!operator &&
|
|
278
|
+
insensitive(input).endsWith(insensitive(matchedField)) &&
|
|
232
279
|
fieldSeparated.length === 1)
|
|
233
280
|
) {
|
|
234
281
|
continue
|
|
@@ -328,7 +375,10 @@ export const useSuggestions = (
|
|
|
328
375
|
continue
|
|
329
376
|
}
|
|
330
377
|
|
|
331
|
-
if (!matchedOperator ||
|
|
378
|
+
if (!matchedOperator || (
|
|
379
|
+
!value &&
|
|
380
|
+
!isNextCharSpace(input, matchedOperator)
|
|
381
|
+
)) {
|
|
332
382
|
continue
|
|
333
383
|
}
|
|
334
384
|
|
|
@@ -1,41 +1,112 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
import Tokenizr from "tokenizr"
|
|
2
|
+
|
|
3
|
+
export enum TokenType {
|
|
4
|
+
NUMBER = 'number',
|
|
5
|
+
DATETIME = 'datetime',
|
|
6
|
+
OPERATOR = 'operator',
|
|
7
|
+
LOGICAL = 'logical',
|
|
8
|
+
BOOLEAN = 'boolean',
|
|
9
|
+
FIELD = 'field',
|
|
10
|
+
COMMA = 'comma',
|
|
11
|
+
STRING = 'string',
|
|
12
|
+
PARTIAL_VALUE = 'partial-string',
|
|
13
|
+
WHITESPACE = 'whitespace'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
enum Tags {
|
|
17
|
+
HAD_FIELD = 'had-field',
|
|
18
|
+
HAD_VALUE = 'had-value'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let tokenizer = new Tokenizr()
|
|
22
|
+
|
|
23
|
+
tokenizer.rule(/[+-]?[0-9\.]+/, (ctx, match) => {
|
|
24
|
+
ctx.accept(TokenType.NUMBER, parseFloat(match[0])).tag(Tags.HAD_VALUE)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
tokenizer.rule(/\((\d{2}\/\d{2}\/\d{4}[\)']?\s?|\s?DD\/MM\/YYYY)\s?(\d{2}:\d{2}:\d{2}|\s?HH:mm:ss)?\)/, (ctx, match) => {
|
|
28
|
+
ctx.accept(TokenType.DATETIME, parseFloat(match[0])).tag(Tags.HAD_VALUE)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
;[
|
|
32
|
+
/<=?/, />=?/, /!=?/, /=/,
|
|
33
|
+
/in?(?![^\s'"])/i,
|
|
34
|
+
/n(o(t(-(in?)?)?)?)?(?![^\s'"])/i,
|
|
35
|
+
/e(x(i(s(ts?)?)?)?)?(?!\S)/i,
|
|
36
|
+
/d(o(e(s(n(t(-(e(x(i(st?)?)?)?)?)?)?)?)?)?)?(?!\S)/i
|
|
37
|
+
].forEach(re => tokenizer.rule(re, (ctx, match) => {
|
|
38
|
+
if (!ctx.tagged(Tags.HAD_FIELD) && /^[a-z]/i.test(match[0])) {
|
|
39
|
+
ctx.accept(TokenType.FIELD).tag(Tags.HAD_FIELD)
|
|
40
|
+
} else {
|
|
41
|
+
ctx.accept(TokenType.OPERATOR, match[0].toUpperCase())
|
|
42
|
+
}
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
tokenizer.rule(/[a-z][a-z\.\d\-_]*/i, (ctx, match) => {
|
|
46
|
+
const upper = match[0].toUpperCase()
|
|
47
|
+
if (ctx.tagged(Tags.HAD_VALUE)) {
|
|
48
|
+
// we just had a value - it would be followed by AND or OR
|
|
49
|
+
ctx.untag(Tags.HAD_FIELD).untag(Tags.HAD_VALUE).accept(TokenType.LOGICAL, upper)
|
|
50
|
+
} else if (ctx.tagged(Tags.HAD_FIELD)) {
|
|
51
|
+
// we had a field but no value yet - this must be either a boolean or an unquoted string
|
|
52
|
+
if (['TRUE', 'FALSE'].includes(upper)) {
|
|
53
|
+
ctx.accept(TokenType.BOOLEAN, upper === 'TRUE').tag(Tags.HAD_VALUE)
|
|
30
54
|
} else {
|
|
31
|
-
|
|
32
|
-
buffer.push($1)
|
|
33
|
-
results.push(buffer.join(''))
|
|
34
|
-
buffer = []
|
|
35
|
-
return
|
|
36
|
-
}
|
|
55
|
+
ctx.accept(TokenType.PARTIAL_VALUE).tag(Tags.HAD_VALUE)
|
|
37
56
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
} else {
|
|
58
|
+
ctx.accept(TokenType.FIELD).tag(Tags.HAD_FIELD)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
tokenizer.rule(/,/, (ctx) => {
|
|
63
|
+
// untag HAD_VALUE because a comma cannot be followed by AND or OR - but by another value
|
|
64
|
+
ctx.untag(Tags.HAD_VALUE).accept(TokenType.COMMA)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
tokenizer.rule(/(?<!\\)"(.*?)(?<!\\)"/, (ctx, match) => {
|
|
68
|
+
ctx.accept(TokenType.STRING, match[1].replace(/\\"/g, '"')).tag(Tags.HAD_VALUE)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
tokenizer.rule(/(?<!\\)'(.*?)(?<!\\)'/, (ctx, match) => {
|
|
72
|
+
ctx.accept(TokenType.STRING, match[1].replace(/\\'/g, "'")).tag(Tags.HAD_VALUE)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
tokenizer.rule(/(?<!\\)['"](.*)/, (ctx, match) => {
|
|
76
|
+
// partial string
|
|
77
|
+
ctx.accept(TokenType.PARTIAL_VALUE, match[1].replace(/\\'/g, "'")).tag(Tags.HAD_VALUE)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
tokenizer.rule(/\s+/, (ctx) => {
|
|
81
|
+
ctx.accept(TokenType.WHITESPACE)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
tokenizer.rule(/.+/, (ctx, match) => {
|
|
85
|
+
// unrecognized token
|
|
86
|
+
ctx.accept(TokenType.WHITESPACE, match[0].replaceAll(/./g, ' '))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
export function tokenize(input: string) {
|
|
90
|
+
tokenizer.reset()
|
|
91
|
+
tokenizer.input(input)
|
|
92
|
+
return tokenizer.tokens()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function splitByQuotes(input: string, ignore?: string) {
|
|
96
|
+
const parts = tokenize(input)
|
|
97
|
+
.filter(token => token.type !== 'whitespace')
|
|
98
|
+
.map(token => token.text)
|
|
99
|
+
.map((text, index, array) => {
|
|
100
|
+
if(array[index + 1] === ',') {
|
|
101
|
+
return text + ','
|
|
102
|
+
} else {
|
|
103
|
+
return text === ',' ? '' : text
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
.filter(text => text !== '')
|
|
107
|
+
|
|
108
|
+
if (/\s$/.test(input)) {
|
|
109
|
+
parts.push('')
|
|
110
|
+
}
|
|
111
|
+
return parts
|
|
41
112
|
}
|