terrier-engine 4.4.21 → 4.4.23

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.
@@ -14,7 +14,8 @@ import {DdDive} from "../gen/models"
14
14
  import Ids from "../../terrier/ids"
15
15
  import Db from "../dd-db"
16
16
  import DdSession from "../dd-session"
17
- import {DiveRunModal} from "./dive-runs";
17
+ import {DiveRunModal} from "./dive-runs"
18
+ import Nav from "tuff-core/nav";
18
19
 
19
20
  const log = new Logger("DiveEditor")
20
21
 
@@ -34,6 +35,8 @@ export default class DiveEditor extends ContentPart<DiveEditorState> {
34
35
 
35
36
  newQueryKey = messages.untypedKey()
36
37
 
38
+ static readonly diveChangedKey = messages.untypedKey()
39
+
37
40
  queries = new Array<Query>()
38
41
 
39
42
  async init() {
@@ -107,6 +110,7 @@ export class DiveEditorPage extends PagePart<{id: string}> {
107
110
  session!: DdSession
108
111
 
109
112
  saveKey = messages.untypedKey()
113
+ discardKey = messages.untypedKey()
110
114
  runKey = messages.untypedKey()
111
115
 
112
116
  async init() {
@@ -129,9 +133,17 @@ export class DiveEditorPage extends PagePart<{id: string}> {
129
133
  icon: 'glyp-data_dive'
130
134
  })
131
135
 
136
+ this.addAction({
137
+ title: 'Discard',
138
+ icon: 'glyp-cancelled',
139
+ classes: ['discard-dive-action'],
140
+ click: {key: this.discardKey}
141
+ }, 'tertiary')
142
+
132
143
  this.addAction({
133
144
  title: 'Save',
134
- icon: 'glyp-checkmark',
145
+ icon: 'glyp-complete',
146
+ classes: ['save-dive-action'],
135
147
  click: {key: this.saveKey}
136
148
  }, 'tertiary')
137
149
 
@@ -141,10 +153,20 @@ export class DiveEditorPage extends PagePart<{id: string}> {
141
153
  click: {key: this.runKey}
142
154
  }, 'tertiary')
143
155
 
156
+ this.onClick(this.discardKey, _ => {
157
+ log.info("Discarding dive changes")
158
+ Nav.visit(location.href)
159
+ })
160
+
144
161
  this.onClick(this.saveKey, _ => this.save())
145
162
 
146
163
  this.onClick(this.runKey, _ => this.run())
147
164
 
165
+ this.listenMessage(DiveEditor.diveChangedKey, _ => {
166
+ log.info("Dive changed")
167
+ this.element?.classList.add('changed')
168
+ }, {attach: 'passive'})
169
+
148
170
  this.dirty()
149
171
  }
150
172
 
@@ -162,6 +184,7 @@ export class DiveEditorPage extends PagePart<{id: string}> {
162
184
  const res = await Db().upsert('dd_dive', dive)
163
185
  if (res.status == 'success') {
164
186
  this.successToast(`Saved Dive!`)
187
+ this.element?.classList.remove('changed')
165
188
  }
166
189
  else {
167
190
  this.alertToast(res.message)
@@ -9,6 +9,7 @@ import Objects from "tuff-core/objects"
9
9
  import {ModalPart} from "../../terrier/modals";
10
10
  import TerrierFormPart from "../../terrier/parts/terrier-form-part"
11
11
  import {Dropdown} from "../../terrier/dropdowns"
12
+ import DiveEditor from "../dives/dive-editor"
12
13
 
13
14
  const log = new Logger("Columns")
14
15
 
@@ -247,6 +248,7 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
247
248
  })
248
249
  const tableData = await this.tableFields.serialize()
249
250
  this.state.tableView.updateColumns(columns, tableData.prefix)
251
+ this.emitMessage(DiveEditor.diveChangedKey, {})
250
252
  this.pop()
251
253
  }
252
254
 
@@ -9,8 +9,9 @@ import inflection from "inflection"
9
9
  import {ModalPart} from "../../terrier/modals";
10
10
  import TerrierFormPart from "../../terrier/parts/terrier-form-part"
11
11
  import {Dropdown} from "../../terrier/dropdowns"
12
- import dayjs from "dayjs";
13
- import Format from "../../terrier/format";
12
+ import dayjs from "dayjs"
13
+ import Format from "../../terrier/format"
14
+ import DiveEditor from "../dives/dive-editor"
14
15
 
15
16
  const log = new Logger("Filters")
16
17
 
@@ -170,7 +171,8 @@ export class FiltersEditorModal extends ModalPart<FiltersEditorState> {
170
171
 
171
172
  this.addAction({
172
173
  title: 'Add Filter',
173
- icon: 'glyp-plus',
174
+ icon: 'glyp-plus_outline',
175
+ classes: ['add-filter'],
174
176
  click: {key: addKey}
175
177
  }, 'secondary')
176
178
 
@@ -183,12 +185,7 @@ export class FiltersEditorModal extends ModalPart<FiltersEditorState> {
183
185
  })
184
186
 
185
187
  this.onClick(addKey, m => {
186
- const onSelected = (filter: Filter) => {
187
- log.info(`Adding ${filter.filter_type} filter`, filter)
188
- this.addState(filter)
189
- this.updateFilterEditors()
190
- }
191
- this.toggleDropdown(AddFilterDropdown, {modelDef: this.modelDef, callback: onSelected}, m.event.target)
188
+ this.showAddFilterDropdown(m.event.target! as HTMLElement)
192
189
  })
193
190
  }
194
191
 
@@ -213,12 +210,30 @@ export class FiltersEditorModal extends ModalPart<FiltersEditorState> {
213
210
  }
214
211
  }
215
212
 
213
+ showAddFilterDropdown(target: HTMLElement | null) {
214
+ const onSelected = (filter: Filter) => {
215
+ log.info(`Adding ${filter.filter_type} filter`, filter)
216
+ this.addState(filter)
217
+ this.updateFilterEditors()
218
+ }
219
+ this.toggleDropdown(AddFilterDropdown, {modelDef: this.modelDef, callback: onSelected}, target)
220
+ }
221
+
222
+ update(elem: HTMLElement) {
223
+ super.update(elem)
224
+
225
+ // if there are no filters, show the dropdown right away
226
+ if (this.filterStates.length == 0) {
227
+ this.showAddFilterDropdown(null)
228
+ }
229
+ }
216
230
 
217
231
  save() {
218
232
  const filters = this.filterStates.map(state => {
219
233
  return Objects.omit(state, 'schema', 'filtersEditor', 'id') as Filter
220
234
  })
221
235
  this.state.tableView.updateFilters(filters)
236
+ this.emitMessage(DiveEditor.diveChangedKey, {})
222
237
  this.pop()
223
238
  }
224
239
 
@@ -538,7 +553,7 @@ type AddFilterCallback = (filter: Filter) => any
538
553
  const columnSelectedKey = messages.typedKey<{column: string}>()
539
554
 
540
555
  class AddFilterDropdown extends Dropdown<{modelDef: ModelDef, callback: AddFilterCallback}> {
541
- columns!: string[]
556
+ columns!: ColumnDef[]
542
557
 
543
558
  get autoClose(): boolean {
544
559
  return true
@@ -547,7 +562,11 @@ class AddFilterDropdown extends Dropdown<{modelDef: ModelDef, callback: AddFilte
547
562
  async init() {
548
563
  await super.init()
549
564
 
550
- this.columns = Object.keys(this.state.modelDef.columns).sort()
565
+ this.columns = arrays.sortByFunction(Object.values(this.state.modelDef.columns), col => {
566
+ const visibility = col.metadata?.visibility
567
+ const sort = visibility == 'common' ? '0' : '1'
568
+ return `${sort}${col.name}`
569
+ })
551
570
 
552
571
  this.onClick(columnSelectedKey, m => {
553
572
  const column = m.data.column
@@ -574,7 +593,7 @@ class AddFilterDropdown extends Dropdown<{modelDef: ModelDef, callback: AddFilte
574
593
  }
575
594
 
576
595
  get parentClasses(): Array<string> {
577
- return super.parentClasses.concat(['dd-select-columns-dropdown']);
596
+ return super.parentClasses.concat(['dd-select-columns-dropdown', 'tt-actions-dropdown']);
578
597
  }
579
598
 
580
599
  renderContent(parent: PartTag) {
@@ -582,9 +601,22 @@ class AddFilterDropdown extends Dropdown<{modelDef: ModelDef, callback: AddFilte
582
601
  header.i(".glyp-columns")
583
602
  header.span().text("Select a Column")
584
603
  })
604
+ let showingCommon = true
585
605
  for (const column of this.columns) {
586
- parent.a({text: column})
587
- .emitClick(columnSelectedKey, {column})
606
+ const isCommon = column.metadata?.visibility == 'common'
607
+ parent.a(a => {
608
+ a.div('.title').text(column.name)
609
+ const desc = column.metadata?.description
610
+ if (desc?.length) {
611
+ a.div('.subtitle').text(desc)
612
+ }
613
+
614
+ // show a border between common and uncommon columns
615
+ if (showingCommon && !isCommon) {
616
+ a.class('border-top')
617
+ }
618
+ }).emitClick(columnSelectedKey, {column: column.name})
619
+ showingCommon = isCommon
588
620
  }
589
621
  }
590
622
 
@@ -3,12 +3,13 @@ import Schema, {BelongsToDef, ModelDef, SchemaDef} from "../../terrier/schema"
3
3
  import inflection from "inflection"
4
4
  import Filters, {Filter, FilterInput, FiltersEditorModal} from "./filters"
5
5
  import Columns, {ColumnRef, ColumnsEditorModal} from "./columns"
6
- import {messages} from "tuff-core"
6
+ import {arrays, messages} from "tuff-core"
7
7
  import {Logger} from "tuff-core/logging"
8
8
  import ContentPart from "../../terrier/parts/content-part"
9
9
  import {ActionsDropdown} from "../../terrier/dropdowns"
10
10
  import {ModalPart} from "../../terrier/modals"
11
11
  import TerrierFormPart from "../../terrier/parts/terrier-form-part"
12
+ import DiveEditor from "../dives/dive-editor";
12
13
 
13
14
  const log = new Logger("Tables")
14
15
 
@@ -98,19 +99,37 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
98
99
  this.app.showModal(FiltersEditorModal, {schema: this.schema, tableView: this as TableView<TableRef>})
99
100
  })
100
101
 
102
+ // show the new join dropdown
101
103
  this.onClick(this.newJoinedKey, m => {
102
104
  log.info(`Adding join to ${this.displayName}`)
103
105
 
104
106
  // only show belongs-tos that aren't already joined
105
107
  const existingJoins = new Set(Object.keys(this.table.joins || []))
106
- const actions = Object.values(this.modelDef.belongs_to)
108
+
109
+ const newJoins = Object.values(this.modelDef.belongs_to)
107
110
  .filter(bt => !existingJoins.has(bt.name))
108
- .map(bt => {
109
- return {
110
- title: Schema.belongsToDisplay(bt),
111
- click: {key: this.createJoinedKey, data: {name: bt.name}}
112
- }
113
- })
111
+
112
+ // show the common tables at the top
113
+ let showingCommon = true
114
+ const actions = arrays.sortByFunction(newJoins, bt => {
115
+ const model = this.schema.models[bt.model]
116
+ const common = model.metadata?.visibility == 'common' ? '0' : '1'
117
+ return `${common}${bt.name}`
118
+ })
119
+ .map(bt => {
120
+ const model = this.schema.models[bt.model]
121
+ const isCommon = model.metadata?.visibility == 'common'
122
+ // put a border between the common and uncommon
123
+ const classes = showingCommon && !isCommon ? ['border-top'] : []
124
+ showingCommon = isCommon
125
+ return {
126
+ title: Schema.belongsToDisplay(bt),
127
+ subtitle: model.metadata?.description,
128
+ classes,
129
+ click: {key: this.createJoinedKey, data: {name: bt.name}}
130
+ }
131
+ })
132
+
114
133
 
115
134
  // don't show the dropdown if there are no more belongs-tos left
116
135
  if (actions.length) {
@@ -121,6 +140,7 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
121
140
  }
122
141
  })
123
142
 
143
+ // create a new join
124
144
  this.onClick(this.createJoinedKey, m => {
125
145
  const belongsTo = this.modelDef.belongs_to[m.data.name]
126
146
  if (belongsTo) {
@@ -372,6 +392,7 @@ class JoinedTableEditorModal extends ModalPart<JoinedTableEditorState> {
372
392
  this.onClick(this.applyKey, async _ => {
373
393
  const table = await this.form.serialize()
374
394
  this.state.callback(table)
395
+ this.emitMessage(DiveEditor.diveChangedKey, {})
375
396
  this.pop()
376
397
  })
377
398
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "*"
6
6
  ],
7
- "version": "4.4.21",
7
+ "version": "4.4.23",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Terrier-Tech/terrier-engine"
@@ -78,9 +78,15 @@ export abstract class Dropdown<TState> extends TerrierPart<TState> {
78
78
 
79
79
  update(_elem: HTMLElement) {
80
80
  const content = _elem.querySelector('.tt-dropdown-content')
81
- if (this.anchorTarget && content) {
82
- log.info(`Anchoring dropdown`, content, this.anchorTarget)
83
- Overlays.anchorElement(content as HTMLElement, this.anchorTarget)
81
+ if (content) {
82
+ if (this.anchorTarget) {
83
+ log.info(`Anchoring dropdown`, content, this.anchorTarget)
84
+ Overlays.anchorElement(content as HTMLElement, this.anchorTarget)
85
+ }
86
+ else {
87
+ // no anchor, just center it on the screen
88
+ Overlays.centerElement(content as HTMLElement)
89
+ }
84
90
  content.classList.add('show')
85
91
  }
86
92
  }
@@ -273,24 +273,40 @@ function anchorBox(size: Size, anchor: Box, container: Size, options: AnchorOpti
273
273
  return preferredResult
274
274
  }
275
275
 
276
+
276
277
  /**
277
- * Anchors one element to the side of another.
278
- * @param elem the element to reposition
279
- * @param anchor the anchor element
278
+ * Gets the size of an element, forcing the browser to calculate it if necessary.
279
+ * @param elem
280
280
  */
281
- function anchorElement(elem: HTMLElement, anchor: HTMLElement) {
281
+ function getElementSize(elem: HTMLElement): Size {
282
282
  // Sometimes the actual width and height of the rendered element is incorrect before we set the style attribute.
283
283
  // Setting the style attribute first forces the browser to re-calculate the size of the element so that we can use
284
284
  // the "real" size to calculate where to anchor the element.
285
285
  elem.setAttribute('style', 'top:0;left:0;')
286
286
 
287
- const elemSize = {
287
+ return {
288
288
  width: elem.offsetWidth,
289
289
  height: elem.offsetHeight
290
290
  }
291
+ }
292
+
293
+ /**
294
+ * Gets the current size of the browser window.
295
+ */
296
+ function getWindowSize(): Size {
297
+ return {width: window.innerWidth, height: window.innerHeight}
298
+ }
299
+
300
+ /**
301
+ * Anchors one element to the side of another.
302
+ * @param elem the element to reposition
303
+ * @param anchor the anchor element
304
+ */
305
+ function anchorElement(elem: HTMLElement, anchor: HTMLElement) {
306
+ const elemSize = getElementSize(elem)
291
307
  log.debug(`Anchoring element`, elem, anchor)
292
308
  const rect = anchor.getBoundingClientRect()
293
- const win = {width: window.innerWidth, height: window.innerHeight }
309
+ const win = getWindowSize()
294
310
  const anchorResult = anchorBox(elemSize, rect, win, {preferredSide: 'bottom'})
295
311
 
296
312
  let styleString = ""
@@ -302,6 +318,25 @@ function anchorElement(elem: HTMLElement, anchor: HTMLElement) {
302
318
  elem.setAttribute('style', styleString)
303
319
  }
304
320
 
321
+ /**
322
+ * Centers an element on the page.
323
+ * @param elem
324
+ */
325
+ function centerElement(elem: HTMLElement) {
326
+ const elemSize = getElementSize(elem)
327
+ const win = getWindowSize()
328
+ log.debug(`Centering element`, elem, win)
329
+ const cappedSize = {
330
+ width: Math.min(win.width, elemSize.width),
331
+ height: Math.min(win.height, elemSize.height)
332
+ }
333
+ const styleString = [
334
+ `left: ${(win.width - cappedSize.width)/2}px`,
335
+ `top: ${(win.height - cappedSize.height)/2}px`
336
+ ].join('; ')
337
+ elem.setAttribute('style', styleString)
338
+ }
339
+
305
340
 
306
341
  ////////////////////////////////////////////////////////////////////////////////
307
342
  // Export
@@ -309,6 +344,7 @@ function anchorElement(elem: HTMLElement, anchor: HTMLElement) {
309
344
 
310
345
  const Overlays = {
311
346
  anchorElement,
347
+ centerElement,
312
348
  anchorBox
313
349
  }
314
350
 
@@ -154,13 +154,12 @@ export default abstract class ContentPart<TState> extends TerrierPart<TState> {
154
154
  constructor: { new(p: PartParent, id: string, state: DropdownStateType): DropdownType; },
155
155
  state: DropdownStateType,
156
156
  target: EventTarget | null) {
157
- if (!(target && target instanceof HTMLElement)) {
158
- throw "Trying to show a dropdown without an element target!"
159
- }
160
157
  const dropdown = this.app.addOverlay(constructor, state, 'dropdown')
161
158
  dropdown.parentPart = this
162
- dropdown.anchor(target)
163
- this.app.lastDropdownTarget = target
159
+ if (target && target instanceof HTMLElement) {
160
+ dropdown.anchor(target)
161
+ this.app.lastDropdownTarget = target
162
+ }
164
163
  }
165
164
 
166
165
  clearDropdowns() {
package/terrier/schema.ts CHANGED
@@ -8,7 +8,7 @@ import inflection from "inflection"
8
8
  /**
9
9
  * Possible visibility for models and columns.
10
10
  */
11
- export type MetaVisibility = 'common' | 'uncommon' | 'never'
11
+ export type MetaVisibility = 'common' | 'uncommon' | 'hidden'
12
12
 
13
13
  /**
14
14
  * Definition for a single column in the schema.
@@ -87,12 +87,13 @@ async function get(): Promise<SchemaDef> {
87
87
  * @param belongsTo
88
88
  */
89
89
  function belongsToDisplay(belongsTo: BelongsToDef): string {
90
+ const btName = inflection.titleize(belongsTo.name)
90
91
  if (belongsTo.name != inflection.singularize(inflection.tableize(belongsTo.model))) {
91
92
  // the model is different than the name of the association
92
- return `${belongsTo.model} (${belongsTo.name})`
93
+ return `${btName} (${belongsTo.model})`
93
94
  }
94
95
  else {
95
- return belongsTo.model
96
+ return btName
96
97
  }
97
98
  }
98
99
 
package/terrier/theme.ts CHANGED
@@ -31,6 +31,7 @@ export type Packet = {
31
31
  */
32
32
  export type Action = {
33
33
  title?: string
34
+ subtitle?: string
34
35
  tooltip?: string
35
36
  icon?: IconName
36
37
  href?: string
@@ -98,6 +99,9 @@ export default class Theme {
98
99
  if (action.title?.length) {
99
100
  a.div('.title', {text: action.title})
100
101
  }
102
+ if (action.subtitle?.length) {
103
+ a.div('.subtitle', {text: action.subtitle})
104
+ }
101
105
  else {
102
106
  a.class('icon-only')
103
107
  }