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.
@@ -1,12 +1,10 @@
1
- import {Part, PartTag} from "tuff-core/parts"
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
- filterStates: FilterState[] = []
165
+ editorStates: EditorState[] = []
164
166
  filterCount = 0
165
167
 
166
168
  updateFilterEditors() {
167
- this.assignCollection('filters', FilterEditorContainer, this.filterStates)
169
+ this.assignCollection('filters', FilterEditorContainer, this.editorStates)
168
170
  }
169
171
 
170
172
  addState(filter: Filter) {
171
173
  this.filterCount += 1
172
- this.filterStates.push({
174
+ this.editorStates.push({
173
175
  schema: this.state.schema,
174
- filtersEditor: this,
175
- id: `filter-${this.filterCount}`, ...filter
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.filterStates, f => f.id == id)
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.filterStates = Arrays.without(this.filterStates, filter)
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.filterStates.length == 0) {
255
+ if (this.editorStates.length == 0) {
254
256
  this.showAddFilterDropdown(null)
255
257
  }
256
258
  }
257
259
 
258
- save() {
259
- const filters = this.filterStates.map(state => {
260
- return Objects.omit(state, 'schema', 'filtersEditor', 'id') as Filter
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 BaseFilterState<T extends BaseFilter> = T & {
277
+ type EditorState = {
275
278
  schema: SchemaDef
276
- filtersEditor: FiltersEditorModal
277
- id: string
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 FilterEditor for the specific type of filter.
284
+ * Contains a concrete instance of FilterFields for the specific type of filter.
310
285
  */
311
- class FilterEditorContainer extends Part<FilterState> {
286
+ class FilterEditorContainer extends TerrierPart<EditorState> {
312
287
 
313
- editor?: FilterEditor<any>
288
+ fields?: FilterFields<any>
314
289
 
315
290
  async init() {
316
- this.makeEditor(this.state.filter_type)
291
+ this.makeFields(this.state.filter)
317
292
  }
318
293
 
319
- makeEditor(filterType: FilterType) {
320
- if (this.editor) {
321
- this.removeChild(this.editor)
322
- }
323
- switch (filterType) {
294
+ makeFields(filter: Filter) {
295
+ switch (filter.filter_type) {
324
296
  case 'direct':
325
- this.editor = this.makePart(DirectFilterEditor, this.state as BaseFilterState<DirectFilter>)
297
+ this.fields = new DirectFilterEditor(this, filter as DirectFilter)
326
298
  break
327
299
  case 'inclusion':
328
- this.editor = this.makePart(InclusionFilterEditor, this.state as BaseFilterState<InclusionFilter>)
300
+ this.fields = new InclusionFilterEditor(this, filter as InclusionFilter)
329
301
  break
330
302
  case 'date_range':
331
- this.editor = this.makePart(DateRangeFilterEditor, this.state as BaseFilterState<DateRangeFilter>)
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: FilterState): boolean {
313
+ assignState(state: EditorState): boolean {
342
314
  const changed = super.assignState(state)
343
315
  if (changed) {
344
- this.makeEditor(this.state.filter_type)
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.editor) {
351
- parent.part(this.editor)
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 FilterEditor<DirectFilter> {
370
+ class DirectFilterEditor extends FilterFields<DirectFilter> {
365
371
 
366
372
  numericChangeKey = Messages.untypedKey()
367
373
 
368
- async init() {
369
- await super.init()
374
+ constructor(container: FilterEditorContainer, filter: DirectFilter) {
375
+ super(container, filter)
370
376
 
371
377
  if (this.columnDef?.type == 'cents') {
372
- this.state.column_type = 'cents'
373
- this.state.numeric_value = parseInt(this.state.value) / 100
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.state.column_type = 'number'
377
- this.state.numeric_value = parseFloat(this.state.value)
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
- log.info(`Direct filter for ${this.columnDef?.name} numeric value changed to ${m.value}`)
384
- if (this.state.column_type == 'cents') {
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.state.value = m.value
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.state.column})
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.state.column_type) {
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 FilterEditor<InclusionFilter> {
449
+ class InclusionFilterEditor extends FilterFields<InclusionFilter> {
430
450
 
431
451
  values!: Set<string>
432
452
 
433
- async init() {
434
- await super.init()
453
+ constructor(container: FilterEditorContainer, filter: InclusionFilter) {
454
+ super(container, filter)
435
455
 
436
- this.values = new Set(this.state.in || [])
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.state.in = Array.from(this.values)
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.state.column})
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 FilterEditor<DateRangeFilter> {
508
+ class DateRangeFilterEditor extends FilterFields<DateRangeFilter> {
489
509
 
490
510
  range!: VirtualDateRange
491
511
 
492
- async init() {
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.state.range) {
495
- this.range = this.state.range
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.state.range = this.range
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.state.range = this.range
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.state.column})
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
- input_key: string
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
- const key = `${table.model}.${filter.column}`
669
- const filterInput: FilterInput = {input_key: key,...filter, input_value: ''}
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.input_key = `${filterInput.input_key}#in`
701
+ filterInput.input_name = `${filterInput.input_name} in`
676
702
  break
677
703
  case 'date_range':
678
- filterInput.input_key = `${filterInput.input_key}#range`
704
+ filterInput.input_name = `${filterInput.input_name} range`
679
705
  break
680
706
  case 'direct':
681
- filterInput.input_key = `${filterInput.input_key}#${filter.operator}`
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.input_key}-min`] = range.min
699
- data[`${filter.input_key}-max`] = dayjs(range.max).subtract(1, 'day').format(Dates.literalFormat)
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.input_key] = (parseInt(filter.value)/100).toString()
730
+ data[filter.id] = (parseInt(filter.value)/100).toString()
705
731
  break
706
732
  default:
707
- data[filter.input_key] = filter.value
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.input_key}-${value}`] = 'true'
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.input_key}-min`] as DateLiteral,
734
- max: dayjs(data[`${filter.input_key}-max`]).add(1, 'day').format(Dates.literalFormat) as DateLiteral
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.input_key]
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.input_key]
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.input_key}-${value}`]) {
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.input_key] = 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.34.1",
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.2.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