terrier-engine 4.34.1 → 4.35.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/data-dive/dives/dive-delivery.ts +13 -6
- package/data-dive/dives/dive-editor.ts +11 -3
- package/data-dive/dives/dive-runs.ts +14 -15
- package/data-dive/gen/models.ts +4 -2
- package/data-dive/plots/dive-plot-axes.ts +85 -10
- package/data-dive/plots/dive-plot-editor.ts +54 -11
- package/data-dive/plots/dive-plot-layouts.ts +49 -0
- package/data-dive/plots/dive-plot-list.ts +19 -5
- package/data-dive/plots/dive-plot-render-part.ts +68 -1
- package/data-dive/plots/dive-plot-styles.ts +194 -0
- package/data-dive/plots/dive-plot-traces.ts +77 -51
- package/data-dive/plots/dive-plots.ts +11 -14
- package/data-dive/queries/columns.ts +33 -24
- package/data-dive/queries/filters.ts +144 -118
- package/data-dive/queries/tables.ts +1 -1
- package/package.json +2 -2
- package/terrier/forms.ts +0 -21
- package/terrier/ids.ts +13 -1
- package/terrier/schedules.ts +43 -48
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {PartTag} from "tuff-core/parts"
|
|
2
2
|
import Dates, {DateLiteral, VirtualDatePeriod, VirtualDateRange} from "./dates"
|
|
3
3
|
import {ColumnDef, ModelDef, SchemaDef} from "../../terrier/schema"
|
|
4
4
|
import {TableRef, TableView} from "./tables"
|
|
5
5
|
import {Logger} from "tuff-core/logging"
|
|
6
|
-
import Objects from "tuff-core/objects"
|
|
7
6
|
import * as inflection from "inflection"
|
|
8
7
|
import {ModalPart} from "../../terrier/modals"
|
|
9
|
-
import TerrierFormPart from "../../terrier/parts/terrier-form-part"
|
|
10
8
|
import {Dropdown} from "../../terrier/dropdowns"
|
|
11
9
|
import dayjs from "dayjs"
|
|
12
10
|
import Format from "../../terrier/format"
|
|
@@ -14,6 +12,9 @@ import DiveEditor from "../dives/dive-editor"
|
|
|
14
12
|
import Messages from "tuff-core/messages"
|
|
15
13
|
import Arrays from "tuff-core/arrays"
|
|
16
14
|
import {SelectOptions} from "tuff-core/forms"
|
|
15
|
+
import {TerrierFormFields} from "../../terrier/forms"
|
|
16
|
+
import TerrierPart from "../../terrier/parts/terrier-part"
|
|
17
|
+
import Ids from "../../terrier/ids"
|
|
17
18
|
|
|
18
19
|
const log = new Logger("Filters")
|
|
19
20
|
|
|
@@ -22,6 +23,7 @@ const log = new Logger("Filters")
|
|
|
22
23
|
////////////////////////////////////////////////////////////////////////////////
|
|
23
24
|
|
|
24
25
|
type BaseFilter = {
|
|
26
|
+
id: string
|
|
25
27
|
filter_type: string
|
|
26
28
|
column: string
|
|
27
29
|
editable?: 'optional' | 'required'
|
|
@@ -77,7 +79,7 @@ export type InclusionFilter = BaseFilter & {
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
// currently not implemented, but it would be neat
|
|
80
|
-
export type OrFilter = {
|
|
82
|
+
export type OrFilter = BaseFilter & {
|
|
81
83
|
column: 'or'
|
|
82
84
|
filter_type: 'or'
|
|
83
85
|
where: Filter[]
|
|
@@ -85,7 +87,7 @@ export type OrFilter = {
|
|
|
85
87
|
|
|
86
88
|
export type Filter = DirectFilter | DateRangeFilter | InclusionFilter | OrFilter
|
|
87
89
|
|
|
88
|
-
type FilterType = Filter['filter_type']
|
|
90
|
+
// export type FilterType = Filter['filter_type']
|
|
89
91
|
|
|
90
92
|
|
|
91
93
|
////////////////////////////////////////////////////////////////////////////////
|
|
@@ -160,19 +162,19 @@ export class FiltersEditorModal extends ModalPart<FiltersEditorState> {
|
|
|
160
162
|
|
|
161
163
|
modelDef!: ModelDef
|
|
162
164
|
table!: TableRef
|
|
163
|
-
|
|
165
|
+
editorStates: EditorState[] = []
|
|
164
166
|
filterCount = 0
|
|
165
167
|
|
|
166
168
|
updateFilterEditors() {
|
|
167
|
-
this.assignCollection('filters', FilterEditorContainer, this.
|
|
169
|
+
this.assignCollection('filters', FilterEditorContainer, this.editorStates)
|
|
168
170
|
}
|
|
169
171
|
|
|
170
172
|
addState(filter: Filter) {
|
|
171
173
|
this.filterCount += 1
|
|
172
|
-
this.
|
|
174
|
+
this.editorStates.push({
|
|
173
175
|
schema: this.state.schema,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
modelDef: this.modelDef,
|
|
177
|
+
filter: filter
|
|
176
178
|
})
|
|
177
179
|
}
|
|
178
180
|
|
|
@@ -229,10 +231,10 @@ export class FiltersEditorModal extends ModalPart<FiltersEditorState> {
|
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
removeFilter(id: string) {
|
|
232
|
-
const filter = Arrays.find(this.
|
|
234
|
+
const filter = Arrays.find(this.editorStates, f => f.filter.id == id)
|
|
233
235
|
if (filter) {
|
|
234
236
|
log.info(`Removing filter ${id}`, filter)
|
|
235
|
-
this.
|
|
237
|
+
this.editorStates = Arrays.without(this.editorStates, filter)
|
|
236
238
|
this.updateFilterEditors()
|
|
237
239
|
}
|
|
238
240
|
}
|
|
@@ -250,15 +252,16 @@ export class FiltersEditorModal extends ModalPart<FiltersEditorState> {
|
|
|
250
252
|
super.update(elem)
|
|
251
253
|
|
|
252
254
|
// if there are no filters, show the dropdown right away
|
|
253
|
-
if (this.
|
|
255
|
+
if (this.editorStates.length == 0) {
|
|
254
256
|
this.showAddFilterDropdown(null)
|
|
255
257
|
}
|
|
256
258
|
}
|
|
257
259
|
|
|
258
|
-
save() {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
async save() {
|
|
261
|
+
const editors = this.getCollectionParts('filters') as FilterEditorContainer[]
|
|
262
|
+
const filters = await Promise.all(editors.map(async editor => {
|
|
263
|
+
return await editor.serialize()
|
|
264
|
+
}))
|
|
262
265
|
this.state.tableView.updateFilters(filters)
|
|
263
266
|
this.emitMessage(DiveEditor.diveChangedKey, {})
|
|
264
267
|
this.pop()
|
|
@@ -271,64 +274,33 @@ export class FiltersEditorModal extends ModalPart<FiltersEditorState> {
|
|
|
271
274
|
// Base Editor
|
|
272
275
|
////////////////////////////////////////////////////////////////////////////////
|
|
273
276
|
|
|
274
|
-
type
|
|
277
|
+
type EditorState = {
|
|
275
278
|
schema: SchemaDef
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
type FilterState = BaseFilterState<Filter>
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Base class for editors for specific filter types.
|
|
284
|
-
*/
|
|
285
|
-
abstract class FilterEditor<T extends BaseFilter> extends TerrierFormPart<BaseFilterState<T>> {
|
|
286
|
-
|
|
287
|
-
modelDef!: ModelDef
|
|
288
|
-
columnDef?: ColumnDef
|
|
289
|
-
|
|
290
|
-
async init() {
|
|
291
|
-
this.modelDef = this.state.filtersEditor.modelDef
|
|
292
|
-
this.columnDef = this.modelDef.columns[this.state.column]
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
get parentClasses(): Array<string> {
|
|
296
|
-
return super.parentClasses.concat(['dd-editor-row'])
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
renderActions(row: PartTag) {
|
|
300
|
-
row.div('.actions', actions => {
|
|
301
|
-
actions.a(a => {
|
|
302
|
-
a.i('.glyp-close')
|
|
303
|
-
}).emitClick(removeKey, {id: this.state.id})
|
|
304
|
-
})
|
|
305
|
-
}
|
|
279
|
+
modelDef: ModelDef
|
|
280
|
+
filter: Filter
|
|
306
281
|
}
|
|
307
282
|
|
|
308
283
|
/**
|
|
309
|
-
* Contains a concrete instance of
|
|
284
|
+
* Contains a concrete instance of FilterFields for the specific type of filter.
|
|
310
285
|
*/
|
|
311
|
-
class FilterEditorContainer extends
|
|
286
|
+
class FilterEditorContainer extends TerrierPart<EditorState> {
|
|
312
287
|
|
|
313
|
-
|
|
288
|
+
fields?: FilterFields<any>
|
|
314
289
|
|
|
315
290
|
async init() {
|
|
316
|
-
this.
|
|
291
|
+
this.makeFields(this.state.filter)
|
|
317
292
|
}
|
|
318
293
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
this.removeChild(this.editor)
|
|
322
|
-
}
|
|
323
|
-
switch (filterType) {
|
|
294
|
+
makeFields(filter: Filter) {
|
|
295
|
+
switch (filter.filter_type) {
|
|
324
296
|
case 'direct':
|
|
325
|
-
this.
|
|
297
|
+
this.fields = new DirectFilterEditor(this, filter as DirectFilter)
|
|
326
298
|
break
|
|
327
299
|
case 'inclusion':
|
|
328
|
-
this.
|
|
300
|
+
this.fields = new InclusionFilterEditor(this, filter as InclusionFilter)
|
|
329
301
|
break
|
|
330
302
|
case 'date_range':
|
|
331
|
-
this.
|
|
303
|
+
this.fields = new DateRangeFilterEditor(this, filter as DateRangeFilter)
|
|
332
304
|
break
|
|
333
305
|
}
|
|
334
306
|
}
|
|
@@ -338,68 +310,103 @@ class FilterEditorContainer extends Part<FilterState> {
|
|
|
338
310
|
* in which case we need a new editor since it's dependent on the filter type.
|
|
339
311
|
* @param state
|
|
340
312
|
*/
|
|
341
|
-
assignState(state:
|
|
313
|
+
assignState(state: EditorState): boolean {
|
|
342
314
|
const changed = super.assignState(state)
|
|
343
315
|
if (changed) {
|
|
344
|
-
this.
|
|
316
|
+
this.makeFields(this.state.filter)
|
|
345
317
|
}
|
|
346
318
|
return changed
|
|
347
319
|
}
|
|
348
320
|
|
|
321
|
+
get parentClasses(): Array<string> {
|
|
322
|
+
return super.parentClasses.concat(['dd-editor-row', 'tt-form'])
|
|
323
|
+
}
|
|
324
|
+
|
|
349
325
|
render(parent: PartTag) {
|
|
350
|
-
if (this.
|
|
351
|
-
|
|
326
|
+
if (this.fields) {
|
|
327
|
+
this.fields.render(parent)
|
|
352
328
|
}
|
|
353
329
|
else {
|
|
354
|
-
parent.div('.tt-bubble.alert', {text: `Unknown filter type '${this.state.filter_type}'`})
|
|
330
|
+
parent.div('.tt-bubble.alert', {text: `Unknown filter type '${this.state.filter.filter_type}'`})
|
|
355
331
|
}
|
|
356
332
|
}
|
|
357
333
|
|
|
334
|
+
async serialize(): Promise<Filter> {
|
|
335
|
+
return await this.fields!.serialize()
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Base class for fields for specific filter types.
|
|
342
|
+
*/
|
|
343
|
+
abstract class FilterFields<F extends BaseFilter> extends TerrierFormFields<F> {
|
|
344
|
+
|
|
345
|
+
modelDef!: ModelDef
|
|
346
|
+
columnDef?: ColumnDef
|
|
347
|
+
|
|
348
|
+
protected constructor(readonly container: FilterEditorContainer, filter: F) {
|
|
349
|
+
super(container, filter)
|
|
350
|
+
this.modelDef = container.state.modelDef
|
|
351
|
+
this.columnDef = this.modelDef.columns[this.data.column]
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
abstract render(parent: PartTag): void
|
|
355
|
+
|
|
356
|
+
renderActions(row: PartTag) {
|
|
357
|
+
row.div('.actions', actions => {
|
|
358
|
+
actions.a(a => {
|
|
359
|
+
a.i('.glyp-close')
|
|
360
|
+
}).emitClick(removeKey, {id: this.data.id})
|
|
361
|
+
})
|
|
362
|
+
}
|
|
358
363
|
}
|
|
359
364
|
|
|
365
|
+
|
|
360
366
|
////////////////////////////////////////////////////////////////////////////////
|
|
361
367
|
// Direct Editor
|
|
362
368
|
////////////////////////////////////////////////////////////////////////////////
|
|
363
369
|
|
|
364
|
-
class DirectFilterEditor extends
|
|
370
|
+
class DirectFilterEditor extends FilterFields<DirectFilter> {
|
|
365
371
|
|
|
366
372
|
numericChangeKey = Messages.untypedKey()
|
|
367
373
|
|
|
368
|
-
|
|
369
|
-
|
|
374
|
+
constructor(container: FilterEditorContainer, filter: DirectFilter) {
|
|
375
|
+
super(container, filter)
|
|
370
376
|
|
|
371
377
|
if (this.columnDef?.type == 'cents') {
|
|
372
|
-
this.
|
|
373
|
-
this.
|
|
378
|
+
this.data.column_type = 'cents'
|
|
379
|
+
this.data.numeric_value = parseInt(this.data.value) / 100
|
|
374
380
|
}
|
|
375
381
|
else if (this.columnDef?.type == 'number') {
|
|
376
|
-
this.
|
|
377
|
-
this.
|
|
382
|
+
this.data.column_type = 'number'
|
|
383
|
+
this.data.numeric_value = parseFloat(this.data.value)
|
|
378
384
|
}
|
|
385
|
+
log.info(`Direct filter for ${this.columnDef?.name} initialized`, this.data)
|
|
379
386
|
|
|
380
387
|
// for numeric types, we use a number input and translate the
|
|
381
388
|
// value back to the string value field whenever it changes
|
|
382
|
-
this.onChange(this.numericChangeKey, m => {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
this.state.value = Math.round(parseFloat(m.value)*100).toString()
|
|
389
|
+
this.part.onChange(this.numericChangeKey, m => {
|
|
390
|
+
if (this.data.column_type == 'cents') {
|
|
391
|
+
this.data.value = Math.round(parseFloat(m.value)*100).toString()
|
|
386
392
|
}
|
|
387
393
|
else {
|
|
388
|
-
this.
|
|
394
|
+
this.data.value = m.value
|
|
389
395
|
}
|
|
396
|
+
log.info(`Direct filter for ${this.columnDef?.name} numeric value changed to ${m.value}`, this.data)
|
|
390
397
|
})
|
|
391
398
|
}
|
|
392
399
|
|
|
393
400
|
render(parent: PartTag) {
|
|
394
401
|
parent.div('.column', col => {
|
|
395
|
-
col.div('.tt-readonly-field', {text: this.
|
|
402
|
+
col.div('.tt-readonly-field', {text: this.data.column})
|
|
396
403
|
})
|
|
397
404
|
parent.div('.operator', col => {
|
|
398
405
|
const opts = operatorOptions(this.columnDef?.type || 'text')
|
|
399
406
|
this.select(col, 'operator', opts)
|
|
400
407
|
})
|
|
401
408
|
parent.div('.filter', col => {
|
|
402
|
-
switch (this.
|
|
409
|
+
switch (this.data.column_type) {
|
|
403
410
|
case 'cents':
|
|
404
411
|
col.div('.tt-compound-field', field => {
|
|
405
412
|
field.label().text('$')
|
|
@@ -418,6 +425,19 @@ class DirectFilterEditor extends FilterEditor<DirectFilter> {
|
|
|
418
425
|
this.renderActions(parent)
|
|
419
426
|
}
|
|
420
427
|
|
|
428
|
+
async serialize() {
|
|
429
|
+
const data = await super.serialize()
|
|
430
|
+
if (data.numeric_value != null) {
|
|
431
|
+
if (this.data.column_type == 'cents') {
|
|
432
|
+
data.value = Math.round(data.numeric_value * 100).toString()
|
|
433
|
+
} else {
|
|
434
|
+
data.value = data.numeric_value.toString()
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
log.info(`Direct filter for ${this.columnDef?.name} serialized`, data)
|
|
438
|
+
return data
|
|
439
|
+
}
|
|
440
|
+
|
|
421
441
|
}
|
|
422
442
|
|
|
423
443
|
////////////////////////////////////////////////////////////////////////////////
|
|
@@ -426,15 +446,15 @@ class DirectFilterEditor extends FilterEditor<DirectFilter> {
|
|
|
426
446
|
|
|
427
447
|
const inclusionChangedKey = Messages.typedKey<{value: string}>()
|
|
428
448
|
|
|
429
|
-
class InclusionFilterEditor extends
|
|
449
|
+
class InclusionFilterEditor extends FilterFields<InclusionFilter> {
|
|
430
450
|
|
|
431
451
|
values!: Set<string>
|
|
432
452
|
|
|
433
|
-
|
|
434
|
-
|
|
453
|
+
constructor(container: FilterEditorContainer, filter: InclusionFilter) {
|
|
454
|
+
super(container, filter)
|
|
435
455
|
|
|
436
|
-
this.values = new Set(this.
|
|
437
|
-
this.onChange(inclusionChangedKey, m => {
|
|
456
|
+
this.values = new Set(this.data.in || [])
|
|
457
|
+
this.part.onChange(inclusionChangedKey, m => {
|
|
438
458
|
const val = m.data.value
|
|
439
459
|
const checked = (m.event.target as HTMLInputElement).checked
|
|
440
460
|
log.info(`${val} checkbox changed to ${checked}`)
|
|
@@ -444,13 +464,13 @@ class InclusionFilterEditor extends FilterEditor<InclusionFilter> {
|
|
|
444
464
|
else {
|
|
445
465
|
this.values.delete(val)
|
|
446
466
|
}
|
|
447
|
-
this.
|
|
467
|
+
this.data.in = Array.from(this.values)
|
|
448
468
|
})
|
|
449
469
|
}
|
|
450
470
|
|
|
451
471
|
render(parent: PartTag) {
|
|
452
472
|
parent.div('.column', col => {
|
|
453
|
-
col.div('.tt-readonly-field', {text: this.
|
|
473
|
+
col.div('.tt-readonly-field', {text: this.data.column})
|
|
454
474
|
})
|
|
455
475
|
parent.div('.operator', col => {
|
|
456
476
|
col.span().text("In")
|
|
@@ -485,42 +505,44 @@ const dateRangeRelativeChangedKey = Messages.untypedKey()
|
|
|
485
505
|
const dateRangePeriodChangedKey = Messages.typedKey<{period: string}>()
|
|
486
506
|
const dateRangePreselectKey = Messages.typedKey<VirtualDateRange>()
|
|
487
507
|
|
|
488
|
-
class DateRangeFilterEditor extends
|
|
508
|
+
class DateRangeFilterEditor extends FilterFields<DateRangeFilter> {
|
|
489
509
|
|
|
490
510
|
range!: VirtualDateRange
|
|
491
511
|
|
|
492
|
-
|
|
512
|
+
constructor(container: FilterEditorContainer, filter: DateRangeFilter) {
|
|
513
|
+
super(container, filter)
|
|
514
|
+
|
|
493
515
|
// assume it's a virtual range for the sake of editing it
|
|
494
|
-
if ('period' in this.
|
|
495
|
-
this.range = this.
|
|
516
|
+
if ('period' in this.data.range) {
|
|
517
|
+
this.range = this.data.range
|
|
496
518
|
}
|
|
497
519
|
else {
|
|
498
520
|
// make up a new range
|
|
499
521
|
this.range = {period: 'day', relative: -1}
|
|
500
|
-
this.
|
|
522
|
+
this.data.range = this.range
|
|
501
523
|
}
|
|
502
524
|
|
|
503
|
-
this.onChange(dateRangeRelativeChangedKey, m => {
|
|
525
|
+
this.part.onChange(dateRangeRelativeChangedKey, m => {
|
|
504
526
|
this.range.relative = parseFloat(m.value)
|
|
505
|
-
this.dirty()
|
|
527
|
+
this.part.dirty()
|
|
506
528
|
})
|
|
507
529
|
|
|
508
|
-
this.onChange(dateRangePeriodChangedKey, m => {
|
|
530
|
+
this.part.onChange(dateRangePeriodChangedKey, m => {
|
|
509
531
|
log.info(`Date range period ${m.data.period} changed to ${m.value}`)
|
|
510
532
|
this.range.period = m.data.period as VirtualDatePeriod
|
|
511
|
-
this.dirty()
|
|
533
|
+
this.part.dirty()
|
|
512
534
|
})
|
|
513
535
|
|
|
514
|
-
this.onClick(dateRangePreselectKey, m => {
|
|
536
|
+
this.part.onClick(dateRangePreselectKey, m => {
|
|
515
537
|
this.range = m.data
|
|
516
|
-
this.
|
|
517
|
-
this.dirty()
|
|
538
|
+
this.data.range = this.range
|
|
539
|
+
this.part.dirty()
|
|
518
540
|
})
|
|
519
541
|
}
|
|
520
542
|
|
|
521
543
|
render(parent: PartTag) {
|
|
522
544
|
parent.div('.column', col => {
|
|
523
|
-
col.div('.tt-readonly-field', {text: this.
|
|
545
|
+
col.div('.tt-readonly-field', {text: this.data.column})
|
|
524
546
|
})
|
|
525
547
|
parent.div('.operator', col => {
|
|
526
548
|
col.span().text("Range")
|
|
@@ -598,16 +620,17 @@ class AddFilterDropdown extends Dropdown<{modelDef: ModelDef, callback: AddFilte
|
|
|
598
620
|
const colDef = this.state.modelDef.columns[column]
|
|
599
621
|
if (colDef) {
|
|
600
622
|
this.clear()
|
|
623
|
+
const id = Ids.makeUuid()
|
|
601
624
|
switch (colDef.type) {
|
|
602
625
|
case 'enum':
|
|
603
626
|
const vals = colDef.possible_values || []
|
|
604
|
-
return this.state.callback({filter_type: 'inclusion', column, in: vals})
|
|
627
|
+
return this.state.callback({id, filter_type: 'inclusion', column, in: vals})
|
|
605
628
|
case 'date':
|
|
606
629
|
case 'datetime':
|
|
607
|
-
return this.state.callback({filter_type: 'date_range', column, range: {period: 'year', relative: 0}})
|
|
630
|
+
return this.state.callback({id, filter_type: 'date_range', column, range: {period: 'year', relative: 0}})
|
|
608
631
|
default: // direct
|
|
609
632
|
const colType = colDef.type == 'number' || colDef.type == 'cents' ? colDef.type : 'text'
|
|
610
|
-
return this.state.callback({filter_type: 'direct', column, column_type: colType, operator: 'eq', value: ''})
|
|
633
|
+
return this.state.callback({id, filter_type: 'direct', column, column_type: colType, operator: 'eq', value: '0'})
|
|
611
634
|
}
|
|
612
635
|
}
|
|
613
636
|
else {
|
|
@@ -653,7 +676,7 @@ class AddFilterDropdown extends Dropdown<{modelDef: ModelDef, callback: AddFilte
|
|
|
653
676
|
////////////////////////////////////////////////////////////////////////////////
|
|
654
677
|
|
|
655
678
|
export type FilterInput = Filter & {
|
|
656
|
-
|
|
679
|
+
input_name: string
|
|
657
680
|
input_value: string
|
|
658
681
|
possible_values?: string[]
|
|
659
682
|
}
|
|
@@ -665,20 +688,23 @@ export type FilterInput = Filter & {
|
|
|
665
688
|
* @param filter
|
|
666
689
|
*/
|
|
667
690
|
function toInput(schema: SchemaDef, table: TableRef, filter: Filter): FilterInput {
|
|
668
|
-
|
|
669
|
-
|
|
691
|
+
if (!filter.id?.length) {
|
|
692
|
+
filter.id = Ids.makeRandom(8)
|
|
693
|
+
}
|
|
694
|
+
const name = `${table.model}.${filter.column}`
|
|
695
|
+
const filterInput: FilterInput = {...filter, input_name: name, input_value: ''}
|
|
670
696
|
switch (filter.filter_type) {
|
|
671
697
|
case 'inclusion':
|
|
672
698
|
const modelDef = schema.models[table.model]
|
|
673
699
|
const columnDef = modelDef.columns[filter.column]
|
|
674
700
|
filterInput.possible_values = columnDef.possible_values
|
|
675
|
-
filterInput.
|
|
701
|
+
filterInput.input_name = `${filterInput.input_name} in`
|
|
676
702
|
break
|
|
677
703
|
case 'date_range':
|
|
678
|
-
filterInput.
|
|
704
|
+
filterInput.input_name = `${filterInput.input_name} range`
|
|
679
705
|
break
|
|
680
706
|
case 'direct':
|
|
681
|
-
filterInput.
|
|
707
|
+
filterInput.input_name = `${filterInput.input_name} ${filter.operator}`
|
|
682
708
|
break
|
|
683
709
|
}
|
|
684
710
|
return filterInput
|
|
@@ -695,21 +721,21 @@ function populateRawInputData(filters: FilterInput[]): Record<string,string> {
|
|
|
695
721
|
switch (filter.filter_type) {
|
|
696
722
|
case 'date_range':
|
|
697
723
|
const range = Dates.materializeVirtualRange(filter.range)
|
|
698
|
-
data[`${filter.
|
|
699
|
-
data[`${filter.
|
|
724
|
+
data[`${filter.id}-min`] = range.min
|
|
725
|
+
data[`${filter.id}-max`] = dayjs(range.max).subtract(1, 'day').format(Dates.literalFormat)
|
|
700
726
|
break
|
|
701
727
|
case 'direct':
|
|
702
728
|
switch (filter.column_type) {
|
|
703
729
|
case 'cents':
|
|
704
|
-
data[filter.
|
|
730
|
+
data[filter.id] = (parseInt(filter.value)/100).toString()
|
|
705
731
|
break
|
|
706
732
|
default:
|
|
707
|
-
data[filter.
|
|
733
|
+
data[filter.id] = filter.value
|
|
708
734
|
}
|
|
709
735
|
break
|
|
710
736
|
case 'inclusion':
|
|
711
737
|
for (const value of filter.in) {
|
|
712
|
-
data[`${filter.
|
|
738
|
+
data[`${filter.id}-${value}`] = 'true'
|
|
713
739
|
}
|
|
714
740
|
break
|
|
715
741
|
default:
|
|
@@ -730,8 +756,8 @@ function serializeRawInputData(filters: FilterInput[], data: Record<string, stri
|
|
|
730
756
|
switch (filter.filter_type) {
|
|
731
757
|
case 'date_range':
|
|
732
758
|
const range = {
|
|
733
|
-
min: data[`${filter.
|
|
734
|
-
max: dayjs(data[`${filter.
|
|
759
|
+
min: data[`${filter.id}-min`] as DateLiteral,
|
|
760
|
+
max: dayjs(data[`${filter.id}-max`]).add(1, 'day').format(Dates.literalFormat) as DateLiteral
|
|
735
761
|
}
|
|
736
762
|
const period = Dates.serializePeriod(range)
|
|
737
763
|
filter.input_value = period
|
|
@@ -739,17 +765,17 @@ function serializeRawInputData(filters: FilterInput[], data: Record<string, stri
|
|
|
739
765
|
case 'direct':
|
|
740
766
|
switch (filter.column_type) {
|
|
741
767
|
case 'cents':
|
|
742
|
-
const dollars = data[filter.
|
|
768
|
+
const dollars = data[filter.id]
|
|
743
769
|
filter.input_value = Math.round(parseFloat(dollars)*100).toString()
|
|
744
770
|
break
|
|
745
771
|
default:
|
|
746
|
-
filter.input_value = data[filter.
|
|
772
|
+
filter.input_value = data[filter.id]
|
|
747
773
|
}
|
|
748
774
|
break
|
|
749
775
|
case 'inclusion':
|
|
750
776
|
const values: string[] = []
|
|
751
777
|
for (const value of filter.possible_values || []) {
|
|
752
|
-
if (data[`${filter.
|
|
778
|
+
if (data[`${filter.id}-${value}`]) {
|
|
753
779
|
values.push(value)
|
|
754
780
|
}
|
|
755
781
|
}
|
|
@@ -58,7 +58,7 @@ const updatedKey = Messages.typedKey<TableRef>()
|
|
|
58
58
|
function computeFilterInputs(schema: SchemaDef, table: TableRef, filters: Record<string, FilterInput>) {
|
|
59
59
|
for (const f of table.filters || []) {
|
|
60
60
|
const fi = Filters.toInput(schema, table, f)
|
|
61
|
-
filters[fi.
|
|
61
|
+
filters[fi.id] = fi
|
|
62
62
|
}
|
|
63
63
|
if (table.joins) {
|
|
64
64
|
for (const j of Object.values(table.joins)) {
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"files": [
|
|
5
5
|
"*"
|
|
6
6
|
],
|
|
7
|
-
"version": "4.
|
|
7
|
+
"version": "4.35.0",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "https://github.com/Terrier-Tech/terrier-engine"
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dayjs": "^1.11.7",
|
|
20
20
|
"inflection": "^2.0.1",
|
|
21
21
|
"tuff-core": "latest",
|
|
22
|
-
"tuff-plot": "^0.
|
|
22
|
+
"tuff-plot": "^0.3.0",
|
|
23
23
|
"tuff-sortable": "latest",
|
|
24
24
|
"turbolinks": "^5.2.0"
|
|
25
25
|
},
|
package/terrier/forms.ts
CHANGED
|
@@ -16,7 +16,6 @@ import GlypPicker from "./parts/glyp-picker"
|
|
|
16
16
|
import Glyps from "./glyps"
|
|
17
17
|
import Messages from "tuff-core/messages"
|
|
18
18
|
import {Logger} from "tuff-core/logging"
|
|
19
|
-
import Strings from "tuff-core/strings"
|
|
20
19
|
import Theme, {Action, ColorName, IconName} from "./theme"
|
|
21
20
|
import Objects from "tuff-core/objects"
|
|
22
21
|
import {InlineStyle} from "tuff-core/tags"
|
|
@@ -44,25 +43,6 @@ function getRadioValue(container: HTMLElement, selector: string): string | undef
|
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
|
|
47
|
-
////////////////////////////////////////////////////////////////////////////////
|
|
48
|
-
// Options
|
|
49
|
-
////////////////////////////////////////////////////////////////////////////////
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Computes a `SelectOptions` array by titleizing the values in a plain string array.
|
|
53
|
-
* @param opts
|
|
54
|
-
*/
|
|
55
|
-
function titleizeOptions(opts: readonly string[], blank?: string): SelectOptions {
|
|
56
|
-
const out = opts.map(c => {
|
|
57
|
-
return {value: c, title: Strings.titleize(c)}
|
|
58
|
-
})
|
|
59
|
-
if (blank != undefined) { // don't test length, allow it to be a blank string
|
|
60
|
-
out.unshift({title: blank, value: ''})
|
|
61
|
-
}
|
|
62
|
-
return out
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
46
|
////////////////////////////////////////////////////////////////////////////////
|
|
67
47
|
// Form Fields
|
|
68
48
|
////////////////////////////////////////////////////////////////////////////////
|
|
@@ -322,7 +302,6 @@ class FileCompoundFieldBuilder<T extends Record<string, unknown>, K extends KeyO
|
|
|
322
302
|
////////////////////////////////////////////////////////////////////////////////
|
|
323
303
|
|
|
324
304
|
const Forms = {
|
|
325
|
-
titleizeOptions,
|
|
326
305
|
getRadioValue
|
|
327
306
|
}
|
|
328
307
|
|
package/terrier/ids.ts
CHANGED
|
@@ -5,8 +5,20 @@ function makeUuid(): string {
|
|
|
5
5
|
return URL.createObjectURL(new Blob([])).slice(-36)
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Makes a new random string of the given length.
|
|
11
|
+
* For when a UUID is overkill.
|
|
12
|
+
* @param len the number of characters in the string
|
|
13
|
+
* @return the random string
|
|
14
|
+
*/
|
|
15
|
+
function makeRandom(len: number): string {
|
|
16
|
+
return window.btoa(String.fromCharCode(...window.crypto.getRandomValues(new Uint8Array(length * 2)))).replace(/[+/]/g, "").substring(0, len)
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
const Ids = {
|
|
9
|
-
makeUuid
|
|
20
|
+
makeUuid,
|
|
21
|
+
makeRandom
|
|
10
22
|
}
|
|
11
23
|
|
|
12
24
|
export default Ids
|