@dataloop-ai/components 0.17.63 → 0.17.65

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.17.63",
3
+ "version": "0.17.65",
4
4
  "exports": {
5
5
  ".": "./index.ts",
6
6
  "./models": "./models.ts",
@@ -25,6 +25,7 @@
25
25
  "@dataloop-ai/icons": "^3.0.6",
26
26
  "@types/lodash": "^4.14.184",
27
27
  "chart.js": "^3.9.1",
28
+ "flat": "^5.0.2",
28
29
  "lodash": "^4.17.21",
29
30
  "moment": "^2.29.4",
30
31
  "sass": "^1.51.0",
@@ -45,6 +46,7 @@
45
46
  "@storybook/client-api": "^7.0.4",
46
47
  "@storybook/vue3": "^7.0.4",
47
48
  "@storybook/vue3-vite": "^7.0.4",
49
+ "@types/flat": "^5.0.2",
48
50
  "@types/jsdom": "^16.2.14",
49
51
  "@types/node": "^18.7.18",
50
52
  "@types/resize-observer-browser": "^0.1.7",
@@ -62,6 +62,9 @@ import {
62
62
  setColorOnHover,
63
63
  setBorderOnHover,
64
64
  setBgOnHover,
65
+ setBgOnPressed,
66
+ setBorderOnPressed,
67
+ setTextOnPressed,
65
68
  setIconSize,
66
69
  setIconPadding,
67
70
  setMaxHeight
@@ -291,7 +294,8 @@ export default defineComponent({
291
294
  disabled: this.disabled,
292
295
  flat: this.flat,
293
296
  shaded: this.shaded,
294
- color: this.color
297
+ color: this.color,
298
+ outlined: this.outlined
295
299
  }),
296
300
  '--dl-button-text-color-hover': setColorOnHover({
297
301
  disabled: this.disabled,
@@ -314,13 +318,18 @@ export default defineComponent({
314
318
  filled: this.filled,
315
319
  color: this.color
316
320
  }),
317
- '--dl-button-text-color-pressed': this.shaded
318
- ? 'var(--dl-color-text-buttons)'
319
- : 'var(--dl-button-text-color)',
320
- '--dl-button-bg-pressed': this.shaded
321
- ? 'var(--dl-color-secondary)'
322
- : 'var(--dl-button-bg)',
323
- '--dl-button-border-pressed': 'var(--dl-button-border)'
321
+ '--dl-button-text-color-pressed': setTextOnPressed({
322
+ shaded: this.shaded,
323
+ outlined: this.shaded
324
+ }),
325
+ '--dl-button-bg-pressed': setBgOnPressed({
326
+ shaded: this.shaded,
327
+ outlined: this.outlined
328
+ }),
329
+ '--dl-button-border-pressed': setBorderOnPressed({
330
+ shaded: this.shaded,
331
+ outlined: this.outlined
332
+ })
324
333
  }
325
334
  }
326
335
 
@@ -70,6 +70,9 @@ export const setTextColor = ({
70
70
  if (disabled) {
71
71
  return getColor('', 'dl-color-disabled')
72
72
  }
73
+ if (shaded && outlined) {
74
+ return 'var(--dl-color-text-darker-buttons)'
75
+ }
73
76
  if (outlined) {
74
77
  return getColor(textColor, 'dl-color-secondary')
75
78
  }
@@ -111,13 +114,17 @@ export const setBorder = ({
111
114
  disabled,
112
115
  flat,
113
116
  color = '',
114
- shaded
117
+ shaded,
118
+ outlined
115
119
  }: Partial<DlButtonProps>) => {
116
120
  if (disabled) {
117
121
  return flat
118
122
  ? 'var(--dl-color-transparent)'
119
123
  : 'var(--dl-color-separator)'
120
124
  }
125
+ if (shaded && outlined) {
126
+ return 'var(--dl-color-separator)'
127
+ }
121
128
  if (flat || shaded) {
122
129
  return 'var(--dl-color-transparent)'
123
130
  }
@@ -177,3 +184,39 @@ export const setBgOnHover = ({
177
184
 
178
185
  return 'var(--dl-color-panel-background)'
179
186
  }
187
+
188
+ export const setBgOnPressed = ({
189
+ shaded,
190
+ outlined
191
+ }: Partial<DlButtonProps>) => {
192
+ if (shaded && outlined) {
193
+ return 'var(--dl-color-text-buttons)'
194
+ }
195
+ if (shaded) {
196
+ return 'var(--dl-color-secondary)'
197
+ }
198
+ return 'var(--dl-button-bg)'
199
+ }
200
+
201
+ export const setTextOnPressed = ({
202
+ shaded,
203
+ outlined
204
+ }: Partial<DlButtonProps>) => {
205
+ if (shaded && outlined) {
206
+ return 'var(--dl-color-secondary)'
207
+ }
208
+ if (shaded) {
209
+ return 'var(--dl-color-text-buttons)'
210
+ }
211
+ return 'var(--dl-button-text-color)'
212
+ }
213
+
214
+ export const setBorderOnPressed = ({
215
+ shaded,
216
+ outlined
217
+ }: Partial<DlButtonProps>) => {
218
+ if (shaded && outlined) {
219
+ return 'var(--dl-color-secondary)'
220
+ }
221
+ return 'var(--dl-button-border)'
222
+ }
@@ -262,7 +262,7 @@ export default defineComponent({
262
262
 
263
263
  .header {
264
264
  display: flex;
265
- padding: 16px;
265
+ padding: var(--dl-dialog-box-header-padding, 16px);
266
266
  border-bottom: var(--dl-dialog-separator);
267
267
  }
268
268
 
@@ -281,7 +281,7 @@ export default defineComponent({
281
281
 
282
282
  .footer {
283
283
  display: flex;
284
- padding: 20px 16px;
284
+ padding: var(--dl-dialog-box-footer-padding, 20px 16px);
285
285
  border-top: var(--dl-dialog-separator);
286
286
  }
287
287
 
@@ -22,7 +22,7 @@
22
22
  :default-width="width"
23
23
  @save="saveQueryDialogBoxModel = true"
24
24
  @focus="setFocused"
25
- @update:modelValue="handleInputModel"
25
+ @update:modelValue="debouncedInputModel"
26
26
  @dql-edit="jsonEditorModel = !jsonEditorModel"
27
27
  />
28
28
  </div>
@@ -44,6 +44,7 @@
44
44
  <dl-button
45
45
  class="dl-smart-search__buttons--filters"
46
46
  shaded
47
+ outlined
47
48
  size="s"
48
49
  >
49
50
  Saved Filters
@@ -65,6 +66,7 @@
65
66
  v-model="jsonEditorModel"
66
67
  :height="500"
67
68
  :width="800"
69
+ style="--dl-dialog-box-footer-padding: 10px 16px"
68
70
  >
69
71
  <template #header>
70
72
  <dl-dialog-box-header
@@ -90,6 +92,7 @@
90
92
  label="Align Left"
91
93
  flat
92
94
  color="secondary"
95
+ padding="0px 3px"
93
96
  @click="alignJsonText"
94
97
  />
95
98
  </div>
@@ -111,11 +114,13 @@
111
114
  label="Delete Query"
112
115
  flat
113
116
  color="secondary"
117
+ padding="0"
114
118
  @click="handleQueryRemove"
115
119
  />
116
120
  </div>
117
121
  <div class="json-editor__footer-save">
118
122
  <dl-button
123
+ style="margin-right: 14px"
119
124
  outlined
120
125
  label="Save As"
121
126
  @click="saveQueryDialogBoxModel = true"
@@ -152,7 +157,10 @@
152
157
  </div>
153
158
  </template>
154
159
  </dl-dialog-box>
155
- <dl-dialog-box v-model="saveQueryDialogBoxModel">
160
+ <dl-dialog-box
161
+ v-model="saveQueryDialogBoxModel"
162
+ style="--dl-dialog-box-footer-padding: 14px 17px"
163
+ >
156
164
  <template #header>
157
165
  <dl-dialog-box-header
158
166
  title="Save Query"
@@ -162,17 +170,22 @@
162
170
  <template #body>
163
171
  <dl-input
164
172
  v-model="newQueryName"
173
+ title="Query name"
165
174
  style="text-align: center"
166
175
  placeholder="Type query name"
167
176
  />
168
177
  </template>
169
178
  <template #footer>
170
179
  <div class="dl-smart-search__buttons--save">
171
- <dl-button @click="handleSaveQuery">
180
+ <dl-button
181
+ :disabled="!newQueryName"
182
+ outlined
183
+ @click="handleSaveQuery"
184
+ >
172
185
  Save
173
186
  </dl-button>
174
187
  <dl-button
175
- padding="10px"
188
+ :disabled="!newQueryName"
176
189
  @click="handleSaveQuery(true)"
177
190
  >
178
191
  Save and Search
@@ -183,7 +196,15 @@
183
196
  </div>
184
197
  </template>
185
198
  <script lang="ts">
186
- import { defineComponent, PropType, ref } from 'vue-demi'
199
+ import {
200
+ defineComponent,
201
+ PropType,
202
+ ref,
203
+ nextTick,
204
+ toRef,
205
+ onMounted,
206
+ watch
207
+ } from 'vue-demi'
187
208
  import { DlTypography, DlMenu } from '../../../essential'
188
209
  import { DlButton } from '../../../basic'
189
210
  import { DlSelect } from '../../DlSelect'
@@ -209,6 +230,7 @@ import {
209
230
  } from './utils/utils'
210
231
  import { v4 } from 'uuid'
211
232
  import { parseSmartQuery, stringifySmartQuery } from '../../../../utils'
233
+ import { debounce } from 'lodash'
212
234
 
213
235
  export default defineComponent({
214
236
  components: {
@@ -223,7 +245,15 @@ export default defineComponent({
223
245
  DlMenu,
224
246
  DlSelect
225
247
  },
248
+ model: {
249
+ prop: 'modelValue',
250
+ event: 'update:modelValue'
251
+ },
226
252
  props: {
253
+ modelValue: {
254
+ type: Object,
255
+ default: {} as { [key: string]: any }
256
+ },
227
257
  status: {
228
258
  type: Object as PropType<SearchStatus>,
229
259
  default: () => ({ type: 'info', message: '' })
@@ -239,9 +269,9 @@ export default defineComponent({
239
269
  colorSchema: {
240
270
  type: Object as PropType<ColorSchema>,
241
271
  default: () => ({
242
- fields: 'blue',
243
- operators: 'darkgreen',
244
- keywords: 'bold'
272
+ fields: 'var(--dl-color-secondary)',
273
+ operators: 'var(--dl-color-positive)',
274
+ keywords: 'var(--dl-color-medium)'
245
275
  })
246
276
  },
247
277
  isLoading: {
@@ -267,10 +297,17 @@ export default defineComponent({
267
297
  width: {
268
298
  type: String,
269
299
  default: '450px'
300
+ },
301
+ /**
302
+ * If true, the validation will be a closed set based on the schema provided
303
+ */
304
+ strict: {
305
+ type: Boolean,
306
+ default: false
270
307
  }
271
308
  },
272
- emits: ['save-query', 'remove-query', 'search-query'],
273
- setup(props) {
309
+ emits: ['save-query', 'remove-query', 'search-query', 'update:modelValue'],
310
+ setup(props, { emit }) {
274
311
  const inputModel = ref('')
275
312
  const jsonEditorModel = ref(false)
276
313
  const searchBarWidth = ref('100%')
@@ -295,21 +332,30 @@ export default defineComponent({
295
332
  value: ''
296
333
  })
297
334
 
335
+ const strictRef = toRef(props, 'strict')
336
+
298
337
  const { suggestions, error, findSuggestions } = useSuggestions(
299
338
  props.schema,
300
- props.aliases
339
+ props.aliases,
340
+ { strict: strictRef }
301
341
  )
302
342
 
303
343
  const handleInputModel = (value: string) => {
304
344
  inputModel.value = value
305
- const json = JSON.stringify(toJSON(removeBrackets(value)))
306
- const newQuery = replaceWithAliases(json, props.aliases)
345
+ const json = toJSON(removeBrackets(value))
346
+ emit('update:modelValue', json)
347
+ const stringified = JSON.stringify(json)
348
+ const newQuery = replaceWithAliases(stringified, props.aliases)
307
349
  activeQuery.value.query = newQuery
308
- findSuggestions(value)
350
+ nextTick(() => {
351
+ findSuggestions(value)
352
+ })
309
353
  isQuerying.value = false
310
354
  oldInputQuery.value = value
311
355
  }
312
356
 
357
+ const debouncedInputModel = debounce(handleInputModel, 300)
358
+
313
359
  const toJSON = (value: string) => {
314
360
  return parseSmartQuery(
315
361
  replaceWithJsDates(value) ?? inputModel.value
@@ -328,6 +374,23 @@ export default defineComponent({
328
374
  toJSON(inputModel.value)
329
375
  }
330
376
  }
377
+
378
+ const modelRef: any = toRef(props, 'modelValue')
379
+
380
+ watch(modelRef, (val: any) => {
381
+ if (val) {
382
+ const stringQuery = stringifySmartQuery(val)
383
+ debouncedInputModel(stringQuery)
384
+ }
385
+ })
386
+
387
+ onMounted(() => {
388
+ if (props.modelValue) {
389
+ const stringQuery = stringifySmartQuery(props.modelValue)
390
+ debouncedInputModel(stringQuery)
391
+ }
392
+ })
393
+
331
394
  return {
332
395
  uuid: `dl-smart-search-${v4()}`,
333
396
  inputModel,
@@ -349,6 +412,7 @@ export default defineComponent({
349
412
  preventUpdate,
350
413
  selectedOption,
351
414
  handleInputModel,
415
+ debouncedInputModel,
352
416
  setFocused,
353
417
  findSuggestions,
354
418
  toJSON
@@ -634,8 +698,12 @@ export default defineComponent({
634
698
  display: flex;
635
699
  justify-content: space-between;
636
700
  }
637
- &-save > * {
638
- margin: 0px 10px;
701
+ &-delete {
702
+ align-items: center;
703
+ display: flex;
704
+ & > * {
705
+ margin-bottom: 6px;
706
+ }
639
707
  }
640
708
  }
641
709
  .json-query {
@@ -8,6 +8,7 @@
8
8
  volatile
9
9
  full-width
10
10
  :items="tabItems"
11
+ font-size="14px"
11
12
  />
12
13
  <div class="dl-filters-tabs">
13
14
  <dl-tab-panels v-model="currentTab">
@@ -66,15 +66,16 @@
66
66
  v-if="withSaveButton"
67
67
  class="dl-smart-search-input__save-btn-wrapper"
68
68
  >
69
- <dl-button
70
- icon="icon-dl-save"
71
- size="16px"
72
- flat
73
- :disabled="saveStatus"
74
- @click="save"
75
- >
69
+ <div>
70
+ <dl-button
71
+ icon="icon-dl-save"
72
+ size="16px"
73
+ flat
74
+ :disabled="saveStatus"
75
+ @click="save"
76
+ />
76
77
  <dl-tooltip> Save Query </dl-tooltip>
77
- </dl-button>
78
+ </div>
78
79
  <dl-button
79
80
  icon="icon-dl-edit"
80
81
  size="16px"
@@ -612,7 +613,7 @@ export default defineComponent({
612
613
  }
613
614
 
614
615
  &--disabled {
615
- border-color: var(--dl-color-disabled);
616
+ border-color: var(--dl-color-separator);
616
617
  }
617
618
  }
618
619
 
@@ -650,7 +651,7 @@ export default defineComponent({
650
651
  height: auto;
651
652
 
652
653
  min-height: 14px;
653
- max-height: 100%;
654
+ max-height: var(--dl-smart-search-bar-wrapper-height);
654
655
  display: block;
655
656
  }
656
657
 
@@ -8,10 +8,7 @@
8
8
  class="query__header"
9
9
  @mousedown="$emit('select')"
10
10
  >
11
- <dl-icon
12
- :icon="icon"
13
- style="margin-bottom: 3px"
14
- />
11
+ <dl-icon :icon="icon" />
15
12
  <span class="query__header--title">
16
13
  {{ name }}
17
14
  </span>
@@ -80,7 +77,7 @@ export default defineComponent({
80
77
  display: flex;
81
78
  align-items: center;
82
79
  &--title {
83
- font-size: 0.5em;
80
+ font-size: 12px;
84
81
  margin: 0px 12px;
85
82
  }
86
83
  }
@@ -95,7 +95,7 @@ function renderText(text: string) {
95
95
  const words = text?.split(/(\s+)/)
96
96
  const output = words?.map((word) => {
97
97
  if (styleModel.keywords.values.includes(word)) {
98
- return `<strong style='${SPAN_STYLES}'>${word}</strong>`
98
+ return `<strong style='${SPAN_STYLES}; color:${styleModel.keywords.color}'>${word}</strong>`
99
99
  } else if (styleModel.fields.values.includes(word)) {
100
100
  return `<span style='color:${styleModel.fields.color}; ${SPAN_STYLES}'>${word}</span>`
101
101
  } else if (styleModel.operators.values.includes(word)) {
@@ -69,12 +69,19 @@ export function replaceWithAliases(json: string, aliases: Alias[]) {
69
69
  })
70
70
  return newJson
71
71
  }
72
- export function revertAliases(json: string, aliases: Alias[]) {
73
- let newJson = json
74
- aliases.forEach((alias) => {
75
- newJson = newJson.replaceAll(alias.key, alias.alias)
76
- })
77
- return newJson
72
+
73
+ export function revertAliases(str: string, aliases: Alias[]) {
74
+ const words: string[] = []
75
+ for (const alias of aliases) {
76
+ words.push(alias.key)
77
+ }
78
+ const replacement = (match: string) => {
79
+ const index = words.indexOf(match)
80
+ return aliases[index].alias
81
+ }
82
+
83
+ const regex = new RegExp(words.join('|'), 'gi')
84
+ return str.replace(regex, replacement)
78
85
  }
79
86
 
80
87
  export function createColorSchema(
@@ -6,26 +6,30 @@
6
6
  dense
7
7
  label="Disabled"
8
8
  />
9
+ <dl-checkbox
10
+ v-model="strictState"
11
+ dense
12
+ label="Strict"
13
+ />
9
14
  </div>
10
15
  <div
11
16
  style="width: 100px"
12
17
  class="props"
13
18
  />
14
19
  <dl-smart-search
20
+ v-model="queryObject"
15
21
  :aliases="aliases"
16
22
  :schema="schema"
17
- :color-schema="{
18
- fields: 'blue',
19
- operators: 'green',
20
- keywords: 'bold'
21
- }"
23
+ :color-schema="colorSchema"
22
24
  :filters="filters"
23
25
  :disabled="switchState"
24
26
  :is-loading="isLoading"
27
+ :strict="strictState"
25
28
  @remove-query="handleRemoveQuery"
26
29
  @save-query="handleSaveQuery"
27
30
  @search-query="handleSearchQuery"
28
31
  />
32
+ {{ queryObject }}
29
33
  </div>
30
34
  </template>
31
35
 
@@ -33,7 +37,6 @@
33
37
  import { defineComponent } from 'vue-demi'
34
38
  import { DlSmartSearch, DlCheckbox } from '../../components'
35
39
  import { Query } from '../../components/types'
36
- import { aliases, schema } from './schema'
37
40
 
38
41
  export default defineComponent({
39
42
  name: 'DlSmartSearchDemo',
@@ -42,11 +45,59 @@ export default defineComponent({
42
45
  DlCheckbox
43
46
  },
44
47
  data() {
48
+ const schema: any = {
49
+ id: ['string', 'number'],
50
+ filename: 'string',
51
+ name: 'string',
52
+ url: 'string',
53
+ type: 'string',
54
+ dataset: 'string',
55
+ datasetId: 'string',
56
+ dir: 'string',
57
+ thumbnail: 'string',
58
+ createdAt: 'date',
59
+ annotated: 'boolean',
60
+ hidden: 'boolean',
61
+ metadata: {
62
+ system: {
63
+ width: 'number',
64
+ height: 'number',
65
+ '*': 'any'
66
+ },
67
+ test: 'any',
68
+ '*': 'any'
69
+ }
70
+ }
71
+
72
+ const colorSchema: any = {
73
+ fields: 'var(--dl-color-secondary)',
74
+ operators: 'var(--dl-color-positive)',
75
+ keywords: 'var(--dl-color-medium)'
76
+ }
77
+
78
+ const aliases: any = [
79
+ {
80
+ alias: 'ItemID',
81
+ key: 'id'
82
+ },
83
+ {
84
+ alias: 'ItemHeight',
85
+ key: 'metadata.system.height'
86
+ },
87
+ {
88
+ alias: 'ItemWidth',
89
+ key: 'metadata.system.width'
90
+ }
91
+ ]
92
+
45
93
  return {
46
94
  schema,
47
95
  aliases,
96
+ colorSchema,
48
97
  switchState: false,
98
+ strictState: false,
49
99
  isLoading: false,
100
+ queryObject: {},
50
101
  filters: {
51
102
  saved: [
52
103
  {
@@ -1,5 +1,7 @@
1
1
  import { Ref, ref } from 'vue-demi'
2
2
  import { splitByQuotes } from '../utils/splitByQuotes'
3
+ import { flatten } from 'flat'
4
+ import { isObject } from 'lodash'
3
5
 
4
6
  export type Schema = {
5
7
  [key: string]:
@@ -50,6 +52,15 @@ const operatorToDataTypeMap: OperatorToDataTypeMap = {
50
52
  $nin: []
51
53
  }
52
54
 
55
+ const knownDataTypes = [
56
+ 'number',
57
+ 'boolean',
58
+ 'string',
59
+ 'date',
60
+ 'datetime',
61
+ 'time'
62
+ ]
63
+
53
64
  type Suggestion = string
54
65
 
55
66
  type Expression = {
@@ -79,14 +90,37 @@ export const dateIntervalPattern = new RegExp(
79
90
  'gi'
80
91
  )
81
92
 
82
- export const useSuggestions = (schema: Schema, aliases: Alias[]) => {
83
- const initialSuggestions = aliases.map((alias) => alias.alias)
84
- const suggestions: Ref<Suggestion[]> = ref(initialSuggestions)
93
+ export const useSuggestions = (
94
+ schema: Schema,
95
+ aliases: Alias[],
96
+ options: { strict?: Ref<boolean> } = {}
97
+ ) => {
98
+ const { strict } = options
99
+ const initialSuggestions = Object.keys(schema)
100
+ const aliasedKeys = aliases.map((alias) => alias.key)
101
+ const aliasedSuggestions = initialSuggestions.map((suggestion) =>
102
+ aliasedKeys.includes(suggestion)
103
+ ? aliases.find((alias) => alias.key === suggestion)?.alias
104
+ : suggestion
105
+ )
106
+
107
+ for (const alias of aliases) {
108
+ if (aliasedSuggestions.includes(alias.alias)) {
109
+ continue
110
+ }
111
+ aliasedSuggestions.push(alias.alias)
112
+ }
113
+
114
+ const sortString = (a: string, b: string) =>
115
+ a.localeCompare(b, undefined, { sensitivity: 'base' })
116
+ const sortedSuggestions = aliasedSuggestions.sort(sortString)
117
+
118
+ const suggestions: Ref<Suggestion[]> = ref(sortedSuggestions)
85
119
  const error: Ref<string | null> = ref(null)
86
120
 
87
121
  const findSuggestions = (input: string) => {
88
122
  input = input.replace(/\s+/g, ' ').trimStart()
89
- localSuggestions = initialSuggestions
123
+ localSuggestions = sortedSuggestions
90
124
 
91
125
  const words = splitByQuotes(input, space)
92
126
  const expressions = mapWordsToExpressions(words)
@@ -110,9 +144,7 @@ export const useSuggestions = (schema: Schema, aliases: Alias[]) => {
110
144
  continue
111
145
  }
112
146
 
113
- const alias = getAliasObjByAlias(aliases, matchedField)
114
- if (!alias) continue
115
- const dataType = getDataTypeByAliasKey(schema, alias.key)
147
+ const dataType = getDataType(schema, aliases, matchedField)
116
148
  if (!dataType) {
117
149
  localSuggestions = []
118
150
  continue
@@ -139,7 +171,9 @@ export const useSuggestions = (schema: Schema, aliases: Alias[]) => {
139
171
  }
140
172
 
141
173
  if (Array.isArray(dataType)) {
142
- localSuggestions = dataType
174
+ localSuggestions = dataType.filter(
175
+ (type) => !knownDataTypes.includes(type)
176
+ )
143
177
 
144
178
  if (!value) continue
145
179
 
@@ -176,11 +210,11 @@ export const useSuggestions = (schema: Schema, aliases: Alias[]) => {
176
210
  if (!matchedKeyword || !isNextCharSpace(input, matchedKeyword))
177
211
  continue
178
212
 
179
- localSuggestions = initialSuggestions
213
+ localSuggestions = sortedSuggestions
180
214
  }
181
215
 
182
216
  error.value = input.length
183
- ? getError(schema, aliases, expressions)
217
+ ? getError(schema, aliases, expressions, { strict })
184
218
  : null
185
219
 
186
220
  suggestions.value = localSuggestions
@@ -194,11 +228,38 @@ const errors = {
194
228
  INVALID_VALUE: (field: string) => `Invalid value for "${field}" field`
195
229
  }
196
230
 
231
+ const isInputAllowed = (input: string, allowedKeys: string[]): boolean => {
232
+ for (const key of allowedKeys) {
233
+ const keyParts = key.split('.')
234
+ const inputParts = input.split('.')
235
+
236
+ if (keyParts.length > inputParts.length) {
237
+ continue
238
+ }
239
+
240
+ let isMatch = true
241
+ for (let i = 0; i < keyParts.length; i++) {
242
+ if (keyParts[i] !== '*' && keyParts[i] !== inputParts[i]) {
243
+ isMatch = false
244
+ break
245
+ }
246
+ }
247
+
248
+ if (isMatch) {
249
+ return true
250
+ }
251
+ }
252
+
253
+ return false
254
+ }
255
+
197
256
  const getError = (
198
257
  schema: Schema,
199
258
  aliases: Alias[],
200
- expressions: Expression[]
259
+ expressions: Expression[],
260
+ options: { strict?: Ref<boolean> } = {}
201
261
  ): string | null => {
262
+ const { strict } = options
202
263
  const hasErrorInStructure = expressions
203
264
  .flatMap((exp) => Object.values(exp))
204
265
  .some((el, index, arr) => {
@@ -214,11 +275,29 @@ const getError = (
214
275
  .filter(({ field, value }) => field !== null && value !== null)
215
276
  .reduce<string | null>((acc, { field, value, operator }, _, arr) => {
216
277
  if (acc === 'warning') return acc
217
- const aliasObj = getAliasObjByAlias(aliases, field)
218
- if (!aliasObj) return 'warning'
278
+ const key: string = getAliasObjByAlias(aliases, field)?.key ?? field
279
+
280
+ /**
281
+ * Handle nested keys to validate if the key exists in the schema or not.
282
+ */
283
+ const keys: string[] = []
284
+ for (const key of Object.keys(schema)) {
285
+ if (isObject(schema[key]) && !Array.isArray(schema[key])) {
286
+ const flattened = flatten({ [key]: schema[key] })
287
+ keys.push(...Object.keys(flattened))
288
+ } else {
289
+ keys.push(key)
290
+ }
291
+ }
292
+
293
+ const isValid = isInputAllowed(key, keys)
294
+ if (!keys.includes(key) && !isValid) {
295
+ return strict.value ? errors.INVALID_EXPRESSION : 'warning'
296
+ }
297
+
219
298
  const valid = isValidByDataType(
220
299
  validateBracketValues(value),
221
- getDataTypeByAliasKey(schema, aliasObj!.key),
300
+ getDataType(schema, aliases, key),
222
301
  operator
223
302
  )
224
303
 
@@ -236,8 +315,16 @@ const isValidByDataType = (
236
315
  dataType: string | string[],
237
316
  operator: string // TODO: use operator
238
317
  ): boolean => {
318
+ if (dataType === 'any') {
319
+ return true
320
+ }
321
+
239
322
  if (Array.isArray(dataType)) {
240
- return !!getValueMatch(dataType, str)
323
+ let isOneOf = !!getValueMatch(dataType, str)
324
+ for (const type of dataType) {
325
+ isOneOf = isOneOf || isValidByDataType(str, type, operator)
326
+ }
327
+ return isOneOf
241
328
  }
242
329
 
243
330
  switch (dataType) {
@@ -283,6 +370,8 @@ const isValidString = (str: string) => {
283
370
  }
284
371
 
285
372
  const getOperatorByDataType = (dataType: string) => {
373
+ if (dataType === 'boolean') return ['$eq', '$neq']
374
+
286
375
  return Object.keys(operatorToDataTypeMap).filter((key) => {
287
376
  const value = operatorToDataTypeMap[key]
288
377
  return value.length === 0 || value.includes(dataType)
@@ -300,19 +389,29 @@ const mapWordsToExpression = (words: string[]): Expression => {
300
389
  }
301
390
  }
302
391
 
303
- const getDataTypeByAliasKey = (
392
+ const getDataType = (
304
393
  schema: Schema,
394
+ aliases: Alias[],
305
395
  key: string
306
396
  ): string | string[] | null => {
307
- const nestedKey = key.split('.')
397
+ const aliasedKey = getAliasObjByAlias(aliases, key)?.key ?? key
398
+
399
+ const nestedKey = aliasedKey.split('.')
308
400
 
309
401
  if (nestedKey.length === 1) {
310
402
  return (schema[nestedKey[0]] as string | string[]) ?? null
311
403
  }
312
404
 
313
405
  let value = schema[nestedKey[0]] as Schema
406
+ if (!value) return null
407
+
314
408
  for (let i = 1; i < nestedKey.length; i++) {
409
+ if (!value) return null
410
+
315
411
  const nextKey = nestedKey[i]
412
+ if (!value[nextKey] && value['*']) {
413
+ return 'any'
414
+ }
316
415
  value = (value[nextKey] as Schema) ?? null
317
416
  }
318
417
 
@@ -1,6 +1,26 @@
1
1
  /* eslint-disable no-empty */
2
2
 
3
- import { isFinite, isObject, isString } from 'lodash'
3
+ import { isBoolean, isFinite, isNumber, isObject, isString } from 'lodash'
4
+
5
+ const GeneratePureValue = (value: any) => {
6
+ if (typeof value === 'string') {
7
+ if (value === 'true') {
8
+ return true
9
+ }
10
+ if (value === 'false') {
11
+ return false
12
+ }
13
+
14
+ try {
15
+ const num = Number(value)
16
+ if (isFinite(num)) {
17
+ return num
18
+ }
19
+ return value.replaceAll('"', '').replaceAll("'", '')
20
+ } catch (e) {}
21
+ }
22
+ return value
23
+ }
4
24
 
5
25
  export const parseSmartQuery = (query: string) => {
6
26
  const queryArr = query.split(' OR ')
@@ -15,44 +35,31 @@ export const parseSmartQuery = (query: string) => {
15
35
  let key: string
16
36
  let value: string | number | object
17
37
 
18
- const cleanValue = (value: any) => {
19
- if (typeof value === 'string') {
20
- try {
21
- const num = Number(value)
22
- if (isFinite(num)) {
23
- return num
24
- }
25
- } catch (e) {}
26
- return value.replaceAll('"', '').replaceAll("'", '')
27
- }
28
- return value
29
- }
30
-
31
38
  for (const term of andTerms) {
32
39
  switch (true) {
33
40
  case term.includes('>='):
34
41
  [key, value] = term.split('>=').map((x) => x.trim())
35
- andQuery[key] = { $gte: cleanValue(value) }
42
+ andQuery[key] = { $gte: GeneratePureValue(value) }
36
43
  break
37
44
  case term.includes('<='):
38
45
  [key, value] = term.split('<=').map((x) => x.trim())
39
- andQuery[key] = { $lte: cleanValue(value) }
46
+ andQuery[key] = { $lte: GeneratePureValue(value) }
40
47
  break
41
48
  case term.includes('>'):
42
49
  [key, value] = term.split('>').map((x) => x.trim())
43
- andQuery[key] = { $gt: cleanValue(value) }
50
+ andQuery[key] = { $gt: GeneratePureValue(value) }
44
51
  break
45
52
  case term.includes('<'):
46
53
  [key, value] = term.split('<').map((x) => x.trim())
47
- andQuery[key] = { $lt: cleanValue(value) }
54
+ andQuery[key] = { $lt: GeneratePureValue(value) }
48
55
  break
49
56
  case term.includes('!='):
50
57
  [key, value] = term.split('!=').map((x) => x.trim())
51
- andQuery[key] = { $ne: cleanValue(value) }
58
+ andQuery[key] = { $ne: GeneratePureValue(value) }
52
59
  break
53
60
  case term.includes('='):
54
61
  [key, value] = term.split('=').map((x) => x.trim())
55
- andQuery[key] = cleanValue(value)
62
+ andQuery[key] = GeneratePureValue(value)
56
63
  break
57
64
  case term.includes('IN'):
58
65
  [key, value] = term.split('IN').map((x) => x.trim())
@@ -64,15 +71,15 @@ export const parseSmartQuery = (query: string) => {
64
71
  .split('NOT-IN')
65
72
  .map((x) => x.trim())[1]
66
73
  .split(',')
67
- .map((x) => cleanValue(x.trim()))
68
- andQuery[key] = { $nin: cleanValue(queryValue) }
74
+ .map((x) => GeneratePureValue(x.trim()))
75
+ andQuery[key] = { $nin: GeneratePureValue(queryValue) }
69
76
  } else {
70
77
  queryValue = term
71
78
  .split('IN')
72
79
  .map((x) => x.trim())[1]
73
80
  .split(',')
74
- .map((x) => cleanValue(x.trim()))
75
- andQuery[key] = { $in: cleanValue(queryValue) }
81
+ .map((x) => GeneratePureValue(x.trim()))
82
+ andQuery[key] = { $in: GeneratePureValue(queryValue) }
76
83
  }
77
84
  break
78
85
  }
@@ -90,104 +97,98 @@ export const stringifySmartQuery = (query: { [key: string]: any }) => {
90
97
  let result = ''
91
98
 
92
99
  for (const key in query) {
93
- if (query.hasOwnProperty(key)) {
94
- const value = query[key]
95
-
96
- if (key === '$or') {
97
- if (Array.isArray(value)) {
98
- const subQueries = value.map(
99
- (subQuery: { [key: string]: any }) =>
100
- stringifySmartQuery(subQuery)
101
- )
102
- result += subQueries.join(' OR ')
103
- }
104
- continue
100
+ const value = query[key]
101
+
102
+ if (key === '$or') {
103
+ if (Array.isArray(value)) {
104
+ const subQueries = value.map(
105
+ (subQuery: { [key: string]: any }) =>
106
+ stringifySmartQuery(subQuery)
107
+ )
108
+ result += subQueries.join(' OR ')
105
109
  }
110
+ continue
111
+ }
106
112
 
107
- if (result.length) {
108
- result += ' AND '
109
- }
113
+ if (result.length) {
114
+ result += ' AND '
115
+ }
110
116
 
111
- if (isObject(value)) {
112
- for (const operator in value) {
113
- if (value.hasOwnProperty(operator)) {
114
- let operatorValue = (
115
- value as {
116
- [key: string]:
117
- | string
118
- | number
117
+ if (isObject(value)) {
118
+ for (const operator in value) {
119
+ if (value.hasOwnProperty(operator)) {
120
+ let operatorValue = (
121
+ value as {
122
+ [key: string]: string | number | string[] | number[]
123
+ }
124
+ )[operator]
125
+ switch (operator) {
126
+ case '$eq':
127
+ result += `${key} = ${
128
+ isString(operatorValue)
129
+ ? `'${operatorValue}'`
130
+ : operatorValue
131
+ }`
132
+ break
133
+ case '$ne':
134
+ result += `${key} != ${
135
+ isString(operatorValue)
136
+ ? `'${operatorValue}'`
137
+ : operatorValue
138
+ }`
139
+ break
140
+ case '$gt':
141
+ result += `${key} > ${operatorValue}`
142
+ break
143
+ case '$gte':
144
+ result += `${key} >= ${operatorValue}`
145
+ break
146
+ case '$lt':
147
+ result += `${key} < ${operatorValue}`
148
+ break
149
+ case '$lte':
150
+ result += `${key} <= ${operatorValue}`
151
+ break
152
+ case '$in':
153
+ if (!Array.isArray(operatorValue)) {
154
+ operatorValue = [operatorValue] as
119
155
  | string[]
120
156
  | number[]
121
157
  }
122
- )[operator]
123
- switch (operator) {
124
- case '$eq':
125
- result += `${key} = ${
126
- isString(operatorValue)
127
- ? `'${operatorValue}'`
128
- : operatorValue
129
- }`
130
- break
131
- case '$ne':
132
- result += `${key} != ${
133
- isString(operatorValue)
134
- ? `'${operatorValue}'`
135
- : operatorValue
136
- }`
137
- break
138
- case '$gt':
139
- result += `${key} > ${operatorValue}`
140
- break
141
- case '$gte':
142
- result += `${key} >= ${operatorValue}`
143
- break
144
- case '$lt':
145
- result += `${key} < ${operatorValue}`
146
- break
147
- case '$lte':
148
- result += `${key} <= ${operatorValue}`
149
- break
150
- case '$in':
151
- if (!Array.isArray(operatorValue)) {
152
- operatorValue = [operatorValue] as
153
- | string[]
154
- | number[]
155
- }
156
-
157
- const inValues: string = (
158
- operatorValue as any[]
158
+
159
+ const inValues: string = (operatorValue as any[])
160
+ .map((x: string | number) =>
161
+ isString(x) ? `'${x}'` : x
159
162
  )
160
- .map((x: string | number) =>
161
- isString(x) ? `'${x}'` : x
162
- )
163
- .join(', ')
164
- result += `${key} IN ${inValues} `
165
- break
166
- case '$nin':
167
- if (!Array.isArray(operatorValue)) {
168
- operatorValue = [operatorValue] as
169
- | string[]
170
- | number[]
171
- }
172
-
173
- const ninValues: string = (
174
- operatorValue as any[]
163
+ .join(', ')
164
+ result += `${key} IN ${inValues} `
165
+ break
166
+ case '$nin':
167
+ if (!Array.isArray(operatorValue)) {
168
+ operatorValue = [operatorValue] as
169
+ | string[]
170
+ | number[]
171
+ }
172
+
173
+ const ninValues: string = (operatorValue as any[])
174
+ .map((x: string | number) =>
175
+ isString(x) ? `'${x}'` : x
175
176
  )
176
- .map((x: string | number) =>
177
- isString(x) ? `'${x}'` : x
178
- )
179
- .join(', ')
180
-
181
- result += `${key} NOT-IN ${ninValues}`
182
- break
183
- default:
184
- throw new Error(`Invalid operator: ${operator}`)
185
- }
177
+ .join(', ')
178
+
179
+ result += `${key} NOT-IN ${ninValues}`
180
+ break
181
+ default:
182
+ throw new Error(`Invalid operator: ${operator}`)
186
183
  }
187
184
  }
188
- } else {
189
- result += `${key} = '${value}'`
190
185
  }
186
+ } else if (isNumber(value)) {
187
+ result += `${key} = ${value}`
188
+ } else if (isBoolean(value)) {
189
+ result += `${key} = ${value}`
190
+ } else {
191
+ result += `${key} = '${value}'`
191
192
  }
192
193
  }
193
194