@dataloop-ai/components 0.20.126 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dataloop-ai/components",
3
- "version": "0.20.126",
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",
@@ -57,6 +57,7 @@ body {
57
57
  --dl-color-fill: #0000001e;
58
58
  --dl-color-fill-hover: #f8f8f8;
59
59
  --dl-color-separator: #e4e4e4;
60
+ --dl-color-component: #ffffff;
60
61
  --dl-color-scrollbar: #e4e4e4;
61
62
  --dl-color-body-background: #eef1ff;
62
63
  --dl-color-panel-background: #ffffff;
@@ -118,6 +119,7 @@ body {
118
119
  --dl-color-fill-secondary: #f8f8f81a;
119
120
  --dl-color-fill-hover: #454a50;
120
121
  --dl-color-separator: #ffffff26;
122
+ --dl-color-component: #30363d;
121
123
  --dl-color-body-background: #24282d;
122
124
  --dl-color-panel-background: #30363d;
123
125
  --dl-color-side-panel: #2a343e;
@@ -223,6 +223,40 @@ export default defineComponent({
223
223
  const onClear = (event: InputEvent) => {
224
224
  emit('clear', event)
225
225
  }
226
+
227
+ function setSelectedLabelByName(displayLabel: string) {
228
+ const stack = [...items.value]
229
+
230
+ while (stack.length) {
231
+ const item = stack.pop()
232
+
233
+ if (item.displayLabel === displayLabel) {
234
+ currentSelectedLabel.value = item
235
+ emit('selected-label', item)
236
+
237
+ nextTick(() => {
238
+ const target = table.value?.$el.querySelector(
239
+ `[data-label-picker-identifier="${item.identifier}"]`
240
+ )
241
+ table.value?.$el
242
+ .querySelectorAll('tr.selected')
243
+ .forEach((tr: Element) =>
244
+ tr.classList.remove('selected')
245
+ )
246
+ target?.closest('tr')?.classList.add('selected')
247
+ })
248
+ return
249
+ }
250
+
251
+ if (item.children?.length) {
252
+ stack.push(...item.children)
253
+ }
254
+ }
255
+ console.warn(
256
+ `[DlLabelPicker] No label found for displayLabel "${displayLabel}"`
257
+ )
258
+ }
259
+
226
260
  return {
227
261
  handleRowClick,
228
262
  inputValue,
@@ -234,7 +268,8 @@ export default defineComponent({
234
268
  isFilterString,
235
269
  onClear,
236
270
  onBlur,
237
- onFocus
271
+ onFocus,
272
+ setSelectedLabelByName
238
273
  }
239
274
  }
240
275
  })
@@ -249,6 +284,14 @@ export default defineComponent({
249
284
  height: 32px;
250
285
  line-height: 30px;
251
286
  }
287
+
288
+ .dl-label-picker .dl-table tbody tr.highlighted td {
289
+ background-color: var(--dl-color-panel-background);
290
+ }
291
+
292
+ .dl-label-picker .dl-table tbody tr.selected td {
293
+ background-color: var(--dl-color-fill);
294
+ }
252
295
  </style>
253
296
 
254
297
  <style>
@@ -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
- const leftover = queryLeftSide.match(/\S+$/)?.[0] || ''
441
- queryLeftSide =
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 = queryLeftSide.trimEnd().split('.')
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
- queryLeftSide = words.join('.')
452
- } else if (
453
- queryLeftSide.endsWith(' ') &&
454
- (queryLeftSide.match(/'/g)?.length ?? 0) % 2 === 0
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 = queryRightSide.match(/^\S+/)?.[0] || ''
462
- queryLeftSide += leftover + ' '
463
- queryRightSide = queryRightSide
464
- .substring(leftover.length)
465
- .trimStart()
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
- if (
473
- /^[^']+((?<!\\)'([^']|(?<=\\)')*(?<!\\)'[^']+)*(?<!\\)'([^']+\\')*[^']*((?<!\\)')?$/.test(
474
- queryLeftSide
475
- )
476
- ) {
477
- queryLeftSide = queryLeftSide.replace(
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?.split(/(\s+)/)
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
- input = input.replace(/\s+/g, ' ').trimStart()
196
- localSuggestions = sortedSuggestions
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 words = splitByQuotes(input, space)
199
- const mergedWords = mergeWords(words)
200
- const expressions = mapWordsToExpressions(mergedWords)
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
- (!isNextCharSpace(input, matchedField) &&
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 || !isNextCharSpace(input, matchedOperator)) {
378
+ if (!matchedOperator || (
379
+ !value &&
380
+ !isNextCharSpace(input, matchedOperator)
381
+ )) {
332
382
  continue
333
383
  }
334
384
 
@@ -1,41 +1,112 @@
1
- export function splitByQuotes(input: string, split: string) {
2
- const pattern = '([\\s\\S]*?)(e)?(?:(o)|(c)|(t)|(sp)|$)'
3
- .replace('sp', split)
4
- .replace('o', '[\\(\\{\\[]')
5
- .replace('c', '[\\)\\}\\]]')
6
- .replace('t', '(?<!\\\\)[\'"]')
7
- .replace('e', '[\\\\]')
8
- const r = new RegExp(pattern, 'gi')
9
- const stack: string[] = []
10
- let buffer: string[] = []
11
- const results: string[] = []
12
- input.replace(r, ($0, $1, $e, $o, $c, $t, $s, i): any => {
13
- if ($e) {
14
- buffer.push($1, $s || $o || $c || $t)
15
- return
16
- } else if ($o) {
17
- if (!stack.includes("'") && !stack.includes('"')) {
18
- stack.push($o)
19
- }
20
- } else if ($c) {
21
- if (!stack.includes("'") && !stack.includes('"')) {
22
- stack.pop()
23
- }
24
- } else if ($t) {
25
- const otherQuote = $t === '"' ? "'" : '"'
26
- if (!stack.includes(otherQuote)) {
27
- if (stack[stack.length - 1] !== $t) stack.push($t)
28
- else stack.pop()
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
- if ($s ? !stack.length : !$1) {
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
- buffer.push($0)
39
- })
40
- return results
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
  }