@dataloop-ai/components 0.20.127 → 0.20.129
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 +3 -2
- package/src/components/compound/DlSearches/DlSmartSearch/components/DlSmartSearchInput.vue +44 -48
- package/src/components/compound/DlSearches/DlSmartSearch/utils/highlightSyntax.ts +2 -1
- package/src/components/compound/DlTreeTable/DlTreeTable.vue +27 -4
- package/src/components/compound/DlTreeTable/views/DlTrTreeView.vue +46 -3
- package/src/demos/DlTreeTableDemo.vue +21 -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.129",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": "./index.ts",
|
|
6
6
|
"./models": "./models.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"check-only": "if grep -E -H -r --exclude-dir=.git --exclude-dir=node_modules --exclude=*.json --exclude=*.yml '^(describe|it).only' .; then echo 'Found only in test files' && exit 1; fi"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@dataloop-ai/icons": "^3.1.
|
|
26
|
+
"@dataloop-ai/icons": "^3.1.22",
|
|
27
27
|
"@types/flat": "^5.0.2",
|
|
28
28
|
"@types/lodash": "^4.14.184",
|
|
29
29
|
"@types/sortablejs": "^1.15.7",
|
|
@@ -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)) {
|
|
@@ -305,6 +305,20 @@ export default defineComponent({
|
|
|
305
305
|
type: Number,
|
|
306
306
|
default: 100
|
|
307
307
|
},
|
|
308
|
+
/**
|
|
309
|
+
* Disable child checkbox when parent is selected
|
|
310
|
+
*/
|
|
311
|
+
disableChildCheckbox: {
|
|
312
|
+
type: Boolean,
|
|
313
|
+
default: false
|
|
314
|
+
},
|
|
315
|
+
/**
|
|
316
|
+
* Tooltip text for disabled child checkbox
|
|
317
|
+
*/
|
|
318
|
+
childDisabledCheckboxTooltip: {
|
|
319
|
+
type: String,
|
|
320
|
+
default: 'Cannot unselect child when parent is selected'
|
|
321
|
+
},
|
|
308
322
|
...useTableActionsProps,
|
|
309
323
|
...commonVirtScrollProps,
|
|
310
324
|
...useTableRowExpandProps,
|
|
@@ -532,12 +546,14 @@ export default defineComponent({
|
|
|
532
546
|
row: DlTableRow,
|
|
533
547
|
index: number,
|
|
534
548
|
children: DlTableRow[] = [],
|
|
535
|
-
level: number = 1
|
|
549
|
+
level: number = 1,
|
|
550
|
+
parentSelected: boolean = false
|
|
536
551
|
) => {
|
|
537
552
|
const currentSlots = {
|
|
538
553
|
default: () => children,
|
|
539
554
|
...computedCellSlots.value
|
|
540
555
|
}
|
|
556
|
+
|
|
541
557
|
return renderComponent(vue2h.value, DlTrTreeView, {
|
|
542
558
|
row,
|
|
543
559
|
rowIndex: index,
|
|
@@ -580,6 +596,10 @@ export default defineComponent({
|
|
|
580
596
|
customIconExpandedRow: props.customIconExpandedRow,
|
|
581
597
|
customIconCompressedRow: props.customIconCompressedRow,
|
|
582
598
|
chevronIconColor: props.chevronIconColor,
|
|
599
|
+
disableChildCheckbox: props.disableChildCheckbox,
|
|
600
|
+
childDisabledCheckboxTooltip:
|
|
601
|
+
props.childDisabledCheckboxTooltip,
|
|
602
|
+
parentSelected,
|
|
583
603
|
'onUpdate:modelValue': (adding: boolean, evt: Event) => {
|
|
584
604
|
updateSelectionHierarchy(adding, evt, row)
|
|
585
605
|
},
|
|
@@ -623,11 +643,14 @@ export default defineComponent({
|
|
|
623
643
|
const renderTr = (
|
|
624
644
|
row: DlTableRow,
|
|
625
645
|
index: number,
|
|
626
|
-
level: number = 1
|
|
646
|
+
level: number = 1,
|
|
647
|
+
parentSelected: boolean = false
|
|
627
648
|
) => {
|
|
628
649
|
const children = []
|
|
650
|
+
const isCurrentRowSelected =
|
|
651
|
+
isRowSelected(props.rowKey, getRowKey.value(row)) === true
|
|
629
652
|
|
|
630
|
-
children.push(renderDlTrTree(row, index, [], level))
|
|
653
|
+
children.push(renderDlTrTree(row, index, [], level, parentSelected))
|
|
631
654
|
|
|
632
655
|
const tbodyEls: VNode[] = []
|
|
633
656
|
|
|
@@ -645,7 +668,7 @@ export default defineComponent({
|
|
|
645
668
|
'data-level': level,
|
|
646
669
|
class: 'nested-tbody'
|
|
647
670
|
},
|
|
648
|
-
renderTr(childRow, i, level)
|
|
671
|
+
renderTr(childRow, i, level, isCurrentRowSelected)
|
|
649
672
|
) as VNode
|
|
650
673
|
)
|
|
651
674
|
})
|
|
@@ -20,12 +20,18 @@
|
|
|
20
20
|
</td>
|
|
21
21
|
<td v-if="hasSelectionMode" class="dl-table--col-auto-width">
|
|
22
22
|
<slot name="body-selection" v-bind="bindBodySelection">
|
|
23
|
+
<dl-tooltip
|
|
24
|
+
v-if="isCheckboxDisabled && childDisabledCheckboxTooltip"
|
|
25
|
+
>
|
|
26
|
+
{{ childDisabledCheckboxTooltip }}
|
|
27
|
+
</dl-tooltip>
|
|
23
28
|
<DlCheckbox
|
|
24
29
|
:color="color"
|
|
25
30
|
:model-value="modelValue"
|
|
26
31
|
:indeterminate-value="true"
|
|
27
32
|
:false-value="false"
|
|
28
33
|
:true-value="true"
|
|
34
|
+
:disabled="isCheckboxDisabled"
|
|
29
35
|
@update:model-value="
|
|
30
36
|
(adding, evt) => emitUpdateModelValue(adding, evt)
|
|
31
37
|
"
|
|
@@ -84,12 +90,14 @@ import {
|
|
|
84
90
|
ref,
|
|
85
91
|
toRefs,
|
|
86
92
|
watch,
|
|
87
|
-
getCurrentInstance
|
|
93
|
+
getCurrentInstance,
|
|
94
|
+
computed
|
|
88
95
|
} from 'vue-demi'
|
|
89
96
|
import DlTrTree from '../components/DlTrTree.vue'
|
|
90
97
|
import DlTdTree from '../components/DlTdTree.vue'
|
|
91
98
|
import DlIcon from '../../../essential/DlIcon/DlIcon.vue'
|
|
92
99
|
import DlCheckbox from '../../../essential/DlCheckbox/DlCheckbox.vue'
|
|
100
|
+
import DlTooltip from '../../../shared/DlTooltip/DlTooltip.vue'
|
|
93
101
|
import { getRowKey } from '../utils/getRowKey'
|
|
94
102
|
import { DlTableRow } from '../../DlTable/types'
|
|
95
103
|
import { setTrPadding } from '../utils/trSpacing'
|
|
@@ -101,7 +109,8 @@ export default defineComponent({
|
|
|
101
109
|
DlTrTree,
|
|
102
110
|
DlTdTree,
|
|
103
111
|
DlIcon,
|
|
104
|
-
DlCheckbox
|
|
112
|
+
DlCheckbox,
|
|
113
|
+
DlTooltip
|
|
105
114
|
},
|
|
106
115
|
props: {
|
|
107
116
|
row: {
|
|
@@ -187,6 +196,27 @@ export default defineComponent({
|
|
|
187
196
|
isRowHighlighted: {
|
|
188
197
|
type: Boolean,
|
|
189
198
|
default: false
|
|
199
|
+
},
|
|
200
|
+
/**
|
|
201
|
+
* Disable child checkbox when parent is selected
|
|
202
|
+
*/
|
|
203
|
+
disableChildCheckbox: {
|
|
204
|
+
type: Boolean,
|
|
205
|
+
default: false
|
|
206
|
+
},
|
|
207
|
+
/**
|
|
208
|
+
* Tooltip text for disabled child checkbox
|
|
209
|
+
*/
|
|
210
|
+
childDisabledCheckboxTooltip: {
|
|
211
|
+
type: String,
|
|
212
|
+
default: 'Cannot unselect child when parent is selected'
|
|
213
|
+
},
|
|
214
|
+
/**
|
|
215
|
+
* Whether the parent row is selected
|
|
216
|
+
*/
|
|
217
|
+
parentSelected: {
|
|
218
|
+
type: Boolean,
|
|
219
|
+
default: false
|
|
190
220
|
}
|
|
191
221
|
},
|
|
192
222
|
emits: [
|
|
@@ -206,6 +236,18 @@ export default defineComponent({
|
|
|
206
236
|
|
|
207
237
|
const vm = getCurrentInstance()
|
|
208
238
|
|
|
239
|
+
const isCheckboxDisabled = computed(() => {
|
|
240
|
+
if (!props.disableChildCheckbox) {
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (props.level === 1) {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return props.parentSelected
|
|
249
|
+
})
|
|
250
|
+
|
|
209
251
|
watch(
|
|
210
252
|
row,
|
|
211
253
|
() => {
|
|
@@ -364,7 +406,8 @@ export default defineComponent({
|
|
|
364
406
|
getExpandedvisibleChildren,
|
|
365
407
|
updateExpandedvisibleChildren,
|
|
366
408
|
onRowHoverStart,
|
|
367
|
-
onRowHoverEnd
|
|
409
|
+
onRowHoverEnd,
|
|
410
|
+
isCheckboxDisabled
|
|
368
411
|
}
|
|
369
412
|
}
|
|
370
413
|
})
|
|
@@ -86,6 +86,14 @@
|
|
|
86
86
|
:model-value="resizableState"
|
|
87
87
|
@update:model-value="updateResizableState"
|
|
88
88
|
/>
|
|
89
|
+
<dl-switch
|
|
90
|
+
left-label="Disable Child Checkbox"
|
|
91
|
+
value="disableChildCheckbox"
|
|
92
|
+
:model-value="disableChildCheckboxState"
|
|
93
|
+
@update:model-value="
|
|
94
|
+
updateDisableChildCheckboxState
|
|
95
|
+
"
|
|
96
|
+
/>
|
|
89
97
|
</div>
|
|
90
98
|
</div>
|
|
91
99
|
</div>
|
|
@@ -109,6 +117,8 @@
|
|
|
109
117
|
style="height: 500px"
|
|
110
118
|
:rows-per-page-options="rowsPerPageOptions"
|
|
111
119
|
highlighted-row="Frozen Yogurt"
|
|
120
|
+
:disable-child-checkbox="disableChildCheckbox"
|
|
121
|
+
child-disabled-checkbox-tooltip="Child checkbox is disabled (parent is selected)"
|
|
112
122
|
@row-click="onRowClick"
|
|
113
123
|
@th-click="log"
|
|
114
124
|
@selected-items="selectedItems"
|
|
@@ -583,6 +593,9 @@ export default defineComponent({
|
|
|
583
593
|
|
|
584
594
|
const nextPageNumber = ref(2)
|
|
585
595
|
|
|
596
|
+
const disableChildCheckbox = ref(false)
|
|
597
|
+
const disableChildCheckboxState = ref([])
|
|
598
|
+
|
|
586
599
|
let allRows: DlTableRow[] = []
|
|
587
600
|
for (let i = 0; i < 100; i++) {
|
|
588
601
|
allRows = allRows.concat(
|
|
@@ -732,7 +745,9 @@ export default defineComponent({
|
|
|
732
745
|
isFirstPage,
|
|
733
746
|
onRowClick,
|
|
734
747
|
rows2,
|
|
735
|
-
columns2
|
|
748
|
+
columns2,
|
|
749
|
+
disableChildCheckbox,
|
|
750
|
+
disableChildCheckboxState
|
|
736
751
|
}
|
|
737
752
|
},
|
|
738
753
|
|
|
@@ -765,6 +780,11 @@ export default defineComponent({
|
|
|
765
780
|
|
|
766
781
|
this.resizable = val.length !== 0
|
|
767
782
|
},
|
|
783
|
+
updateDisableChildCheckboxState(val: boolean[]): void {
|
|
784
|
+
this.disableChildCheckboxState = val
|
|
785
|
+
|
|
786
|
+
this.disableChildCheckbox = val.length !== 0
|
|
787
|
+
},
|
|
768
788
|
log(...args: any[]) {
|
|
769
789
|
console.log(...args)
|
|
770
790
|
}
|
|
@@ -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
|
}
|