terrier-engine 4.4.21 → 4.4.26

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.
@@ -3,19 +3,68 @@ import Api from "../terrier/api"
3
3
  import {DdDiveGroup} from "./gen/models"
4
4
  import {arrays} from "tuff-core";
5
5
  import {SelectOptions} from "tuff-core/forms"
6
+ import {Logger} from "tuff-core/logging"
6
7
 
8
+ const log = new Logger("DD Session")
7
9
 
8
10
  type DdSessionData = {
9
11
  user: DdUser
10
12
  groupMap: Record<string, DdDiveGroup>
11
13
  }
12
14
 
15
+ const showHintsKey = 'dd-show-hints'
16
+
17
+ /**
18
+ * Class that gets added to the body when hints shouldn't be shown.
19
+ * Needs to be negative like this so we can just apply an !important and override any accidental display values.
20
+ */
21
+ const hideHintsClass = 'dd-hide-hints'
22
+
13
23
  /**
14
24
  * Stores authentication information as well as various options from the server.
15
25
  */
16
26
  export default class DdSession {
17
27
 
28
+ _showHints = false
29
+
18
30
  constructor(readonly data: DdSessionData) {
31
+ // default to showing hints
32
+ const rawShowHints = localStorage.getItem(showHintsKey)
33
+ this._showHints = !rawShowHints || rawShowHints == 'true'
34
+ this.updateShowHints()
35
+ }
36
+
37
+ /**
38
+ * Whether to show the helpful hint bubbles all over the place.
39
+ */
40
+ get showHints() {
41
+ return this._showHints
42
+ }
43
+
44
+ /**
45
+ * Stores the showHints preference in localStorage.
46
+ * @param val whether to show hints
47
+ */
48
+ set showHints(val: boolean) {
49
+ this._showHints = val
50
+ log.info(`Persisting ${showHintsKey} to ${val}`)
51
+ localStorage.setItem(showHintsKey, val.toString())
52
+ this.updateShowHints()
53
+ }
54
+
55
+ /**
56
+ * Updates the DOM with the dd-show-hints class based on the value of this._showHints
57
+ * @private
58
+ */
59
+ private updateShowHints() {
60
+ const body = document.querySelector('body')
61
+ if (body) {
62
+ if (this._showHints) {
63
+ body.classList.remove(hideHintsClass)
64
+ } else {
65
+ body.classList.add(hideHintsClass)
66
+ }
67
+ }
19
68
  }
20
69
 
21
70
  get user(): DdUser {
@@ -36,6 +85,9 @@ export default class DdSession {
36
85
  })
37
86
  }
38
87
 
88
+ /**
89
+ * Gets user session information from the server.
90
+ */
39
91
  static async get(): Promise<DdSession> {
40
92
  const data = await Api.safeGet<DdSessionData>("/data_dive/user_session.json", {})
41
93
  return new DdSession(data)
@@ -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
 
@@ -26,6 +27,7 @@ const log = new Logger("DiveEditor")
26
27
  export type DiveEditorState = {
27
28
  schema: SchemaDef
28
29
  dive: DdDive
30
+ session: DdSession
29
31
  }
30
32
 
31
33
  export default class DiveEditor extends ContentPart<DiveEditorState> {
@@ -34,17 +36,26 @@ export default class DiveEditor extends ContentPart<DiveEditorState> {
34
36
 
35
37
  newQueryKey = messages.untypedKey()
36
38
 
39
+ static readonly diveChangedKey = messages.untypedKey()
40
+
37
41
  queries = new Array<Query>()
38
42
 
39
43
  async init() {
40
44
  this.tabs = this.makePart(TabContainerPart, {side: 'top'})
41
45
 
42
- this.tabs.setBeforeAction({
46
+ this.tabs.addBeforeAction({
43
47
  title: 'Queries:',
44
48
  icon: 'glyp-data_dive_query'
45
49
  })
46
- this.tabs.setAfterAction({
50
+ this.tabs.addAfterAction({
51
+ title: "Add Another Query",
52
+ classes: ['dd-hint', 'arrow-right', 'glyp-hint'],
53
+ tooltip: "Each query represents a separate tab in the resulting spreadsheet",
54
+ click: {key: this.newQueryKey}
55
+ })
56
+ this.tabs.addAfterAction({
47
57
  icon: 'glyp-plus_outline',
58
+ classes: ['new-query'],
48
59
  click: {key: this.newQueryKey}
49
60
  })
50
61
 
@@ -106,7 +117,9 @@ export class DiveEditorPage extends PagePart<{id: string}> {
106
117
  editor!: DiveEditor
107
118
  session!: DdSession
108
119
 
120
+ showHintsKey = messages.untypedKey()
109
121
  saveKey = messages.untypedKey()
122
+ discardKey = messages.untypedKey()
110
123
  runKey = messages.untypedKey()
111
124
 
112
125
  async init() {
@@ -115,7 +128,7 @@ export class DiveEditorPage extends PagePart<{id: string}> {
115
128
  const schema = await Schema.get()
116
129
  this.session = await DdSession.get()
117
130
  const dive = await Dives.get(this.state.id)
118
- this.editor = this.makePart(DiveEditor, {schema, dive})
131
+ this.editor = this.makePart(DiveEditor, {schema, dive, session: this.session})
119
132
 
120
133
  this.mainContentWidth = 'wide'
121
134
 
@@ -129,9 +142,25 @@ export class DiveEditorPage extends PagePart<{id: string}> {
129
142
  icon: 'glyp-data_dive'
130
143
  })
131
144
 
145
+ this.addToolbarInput('show-hints', 'checkbox', {
146
+ title: "Hints",
147
+ icon: 'glyp-hint',
148
+ defaultValue: this.session.showHints.toString(),
149
+ onChangeKey: this.showHintsKey,
150
+ onInputKey: this.showHintsKey
151
+ })
152
+
153
+ this.addAction({
154
+ title: 'Discard',
155
+ icon: 'glyp-cancelled',
156
+ classes: ['discard-dive-action'],
157
+ click: {key: this.discardKey}
158
+ }, 'tertiary')
159
+
132
160
  this.addAction({
133
161
  title: 'Save',
134
- icon: 'glyp-checkmark',
162
+ icon: 'glyp-complete',
163
+ classes: ['save-dive-action'],
135
164
  click: {key: this.saveKey}
136
165
  }, 'tertiary')
137
166
 
@@ -141,10 +170,27 @@ export class DiveEditorPage extends PagePart<{id: string}> {
141
170
  click: {key: this.runKey}
142
171
  }, 'tertiary')
143
172
 
173
+ this.onClick(this.discardKey, _ => {
174
+ log.info("Discarding dive changes")
175
+ Nav.visit(location.href)
176
+ })
177
+
144
178
  this.onClick(this.saveKey, _ => this.save())
145
179
 
146
180
  this.onClick(this.runKey, _ => this.run())
147
181
 
182
+ this.onInput(this.showHintsKey, m => {
183
+ // TODO: fix emitInput and emitChange to return the correct value in tuff
184
+ const showHints = (m.event.target as HTMLInputElement).checked
185
+ log.info(`Show hints input to ${showHints}`, m)
186
+ this.session.showHints = showHints
187
+ })
188
+
189
+ this.listenMessage(DiveEditor.diveChangedKey, _ => {
190
+ log.info("Dive changed")
191
+ this.element?.classList.add('changed')
192
+ }, {attach: 'passive'})
193
+
148
194
  this.dirty()
149
195
  }
150
196
 
@@ -162,6 +208,7 @@ export class DiveEditorPage extends PagePart<{id: string}> {
162
208
  const res = await Db().upsert('dd_dive', dive)
163
209
  if (res.status == 'success') {
164
210
  this.successToast(`Saved Dive!`)
211
+ this.element?.classList.remove('changed')
165
212
  }
166
213
  else {
167
214
  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', 'secondary'],
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
 
@@ -39,7 +39,10 @@ class SettingsPart extends ContentPart<SubEditorState> {
39
39
 
40
40
  renderContent(parent: PartTag) {
41
41
  parent.part(this.form)
42
- parent.div('.tt-flex.justify-end', row => {
42
+ parent.div('.tt-flex.gap.align-center.justify-end', row => {
43
+ row.div('.dd-hint.glyp-hint', hint => {
44
+ hint.div('.title').text("These settings only apply to this query, not the dive as a whole")
45
+ })
43
46
  row.a('.alert.tt-flex', a => {
44
47
  a.i('.glyp-delete')
45
48
  a.span({text: "Delete"})
@@ -27,7 +27,7 @@ export default class QueryForm extends ContentPart<{ query: QuerySettings }> {
27
27
 
28
28
 
29
29
  get parentClasses(): Array<string> {
30
- return ['tt-form']
30
+ return ['tt-form', 'tt-flex', 'column', 'gap']
31
31
  }
32
32
 
33
33
  renderContent(parent: PartTag): void {
@@ -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) {
@@ -195,7 +215,17 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
195
215
  a.div().text("Join")
196
216
  a.i('.glyp-belongs_to')
197
217
  }).emitClick(this.newJoinedKey)
218
+
219
+ // join hint
220
+ if (!Object.keys(this.table.joins || {}).length) {
221
+ panel.a('.dd-hint.joins.arrow-top.glyp-hint', hint => {
222
+ hint.div('.title').text("Join More Tables")
223
+ })
224
+ .emitClick(this.newJoinedKey)
225
+ .data({tooltip: "Include data from other tables that are related to this one"})
226
+ }
198
227
  })
228
+
199
229
  }
200
230
 
201
231
  renderColumns(parent: PartTag) {
@@ -216,6 +246,11 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
216
246
  }
217
247
  else {
218
248
  section.div('.line.empty').div().text('None')
249
+ section.div('.dd-hint-container', hintContainer => {
250
+ hintContainer.div('dd-hint.glyp-hint', hint => {
251
+ hint.div('.hint-title').text("Add Columns")
252
+ })
253
+ })
219
254
  }
220
255
  }).emitClick(this.editColumnsKey, {})
221
256
  }
@@ -234,6 +269,11 @@ export class TableView<T extends TableRef> extends ContentPart<{ schema: SchemaD
234
269
  }
235
270
  } else {
236
271
  section.div('.line.empty').text('None')
272
+ section.div('.dd-hint-container', hintContainer => {
273
+ hintContainer.div('dd-hint.glyp-hint', hint => {
274
+ hint.div('.hint-title').text("Add Filters")
275
+ })
276
+ })
237
277
  }
238
278
  }).emitClick(this.editFiltersKey, {})
239
279
  }
@@ -372,6 +412,7 @@ class JoinedTableEditorModal extends ModalPart<JoinedTableEditorState> {
372
412
  this.onClick(this.applyKey, async _ => {
373
413
  const table = await this.form.serialize()
374
414
  this.state.callback(table)
415
+ this.emitMessage(DiveEditor.diveChangedKey, {})
375
416
  this.pop()
376
417
  })
377
418
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "*"
6
6
  ],
7
- "version": "4.4.21",
7
+ "version": "4.4.26",
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
  }
package/terrier/glyps.ts CHANGED
@@ -1,6 +1,6 @@
1
- // This file was automatically generated by glyps:compile on 06/18/23 3:03 PM, DO NOT MANUALLY EDIT!
1
+ // This file was automatically generated by glyps:compile on 07/21/23 5:50 PM, DO NOT MANUALLY EDIT!
2
2
 
3
- const names = ['glyp-abacus', 'glyp-account', 'glyp-accounts_payable', 'glyp-accounts_receivable', 'glyp-action_log', 'glyp-activate', 'glyp-active', 'glyp-activity_high', 'glyp-activity_low', 'glyp-activity_medium', 'glyp-activity_none', 'glyp-address1', 'glyp-adjustment', 'glyp-admin', 'glyp-administration', 'glyp-air_freshener', 'glyp-alert', 'glyp-align_horizontal', 'glyp-align_vertical', 'glyp-anchor', 'glyp-announcement', 'glyp-app_update', 'glyp-append_selection', 'glyp-appointment', 'glyp-appointment_setup', 'glyp-archive', 'glyp-arrow_down', 'glyp-arrow_left', 'glyp-arrow_right', 'glyp-arrow_transfer', 'glyp-arrow_up', 'glyp-ascending', 'glyp-attachment', 'glyp-audit_analyzer', 'glyp-autopay', 'glyp-availability', 'glyp-background_batch', 'glyp-background_job', 'glyp-bait_for_demolition', 'glyp-bar_chart', 'glyp-barcode', 'glyp-bat_exclusion', 'glyp-bed_bug_fumigation', 'glyp-begin_service', 'glyp-belongs_to', 'glyp-billing', 'glyp-billing_office', 'glyp-billing_terms', 'glyp-billto', 'glyp-bioremediation', 'glyp-bird_exclusion', 'glyp-black_light', 'glyp-branch', 'glyp-branch_bats', 'glyp-branch_birds', 'glyp-branch_general', 'glyp-branch_irrigation', 'glyp-branch_landscape', 'glyp-branch_multi_housing', 'glyp-branch_office', 'glyp-branch_orders', 'glyp-branch_residential', 'glyp-branch_sales', 'glyp-branch_termites', 'glyp-branch_weeds', 'glyp-branch_wildlife', 'glyp-build', 'glyp-calculate', 'glyp-calendar', 'glyp-calendar_branch', 'glyp-calendar_map', 'glyp-calendar_rolling', 'glyp-calendar_share', 'glyp-calendar_snap', 'glyp-california', 'glyp-call', 'glyp-callback', 'glyp-camera', 'glyp-cancellation', 'glyp-cancelled', 'glyp-card_american_express', 'glyp-card_discover', 'glyp-card_mastercard', 'glyp-card_visa', 'glyp-catalog', 'glyp-caught', 'glyp-cert', 'glyp-check_all', 'glyp-check_in', 'glyp-check_in_service', 'glyp-checked', 'glyp-checked_empty', 'glyp-checkmark', 'glyp-checkmark_filled', 'glyp-checkout', 'glyp-chemical', 'glyp-chemical_authorization', 'glyp-chemical_backpack_sprayer', 'glyp-chemical_biological_controls', 'glyp-chemical_disinfectant', 'glyp-chemical_fertilizer', 'glyp-chemical_flying_insect_bait', 'glyp-chemical_fumigants', 'glyp-chemical_insect_aerosol', 'glyp-chemical_insect_bait', 'glyp-chemical_insect_dust', 'glyp-chemical_insect_granules', 'glyp-chemical_insect_spray', 'glyp-chemical_label', 'glyp-chemical_mole_bait', 'glyp-chemical_pest_bird_control', 'glyp-chemical_pheromone_products', 'glyp-chemical_power_sprayer', 'glyp-chemical_rodenticide_block', 'glyp-chemical_rodenticide_non_block', 'glyp-chemical_sds', 'glyp-chemical_tree_products', 'glyp-chemical_weed_control', 'glyp-chemical_wildlife_repellents', 'glyp-chemical_wildlife_toxicants', 'glyp-chevron_down', 'glyp-chevron_left', 'glyp-chevron_right', 'glyp-chevron_up', 'glyp-city', 'glyp-click', 'glyp-client_actions', 'glyp-client_pest_sightings', 'glyp-client_required', 'glyp-close', 'glyp-cloud', 'glyp-clypboard', 'glyp-clypedia', 'glyp-clypmart', 'glyp-code', 'glyp-code_details', 'glyp-collections', 'glyp-columns', 'glyp-comment', 'glyp-comment_filled', 'glyp-commission_origin', 'glyp-commissions', 'glyp-company_setup', 'glyp-compass', 'glyp-complete', 'glyp-complete_certificate', 'glyp-complete_enrollment', 'glyp-condition_appliance_machinery', 'glyp-condition_customer', 'glyp-condition_device', 'glyp-condition_door_window', 'glyp-condition_dumpster', 'glyp-condition_entry_point', 'glyp-condition_evidence', 'glyp-condition_exclusion', 'glyp-condition_exterior', 'glyp-condition_interior', 'glyp-condition_plumbing', 'glyp-condition_prep_storage', 'glyp-condition_rodent_evidence', 'glyp-condition_rodent_exclusion', 'glyp-condition_roof_ceiling', 'glyp-condition_structure', 'glyp-condition_supplies', 'glyp-condition_termites', 'glyp-condition_ventilation', 'glyp-condition_wall_floor', 'glyp-condition_wildlife', 'glyp-conditions', 'glyp-consolidate', 'glyp-construction', 'glyp-contract', 'glyp-contract_cancellation', 'glyp-contract_overview', 'glyp-conversation', 'glyp-copesan', 'glyp-copy', 'glyp-credit_memo', 'glyp-credit_non_revenue', 'glyp-credit_production', 'glyp-credit_prospect', 'glyp-credit_sales_bonus', 'glyp-crew', 'glyp-cumulative', 'glyp-cursor', 'glyp-custom_form', 'glyp-customer', 'glyp-customer_service', 'glyp-dashboard', 'glyp-data_dive', 'glyp-data_dive_query', 'glyp-data_dives', 'glyp-database', 'glyp-dead_animal_removal', 'glyp-default', 'glyp-delete', 'glyp-demo_mode', 'glyp-descending', 'glyp-design', 'glyp-desktop', 'glyp-detail_report', 'glyp-developer', 'glyp-device', 'glyp-device_sync', 'glyp-differential', 'glyp-disable', 'glyp-disabled_filled', 'glyp-distribute_horizontal', 'glyp-distribute_vertical', 'glyp-division', 'glyp-document', 'glyp-documentation', 'glyp-documents', 'glyp-download', 'glyp-driving', 'glyp-duplicate', 'glyp-duplicate_location', 'glyp-duration', 'glyp-edit', 'glyp-email', 'glyp-employment', 'glyp-entertainment_public_venues', 'glyp-equipment_bear_trap', 'glyp-equipment_flying_insect_bait_station', 'glyp-equipment_flying_insect_light_trap', 'glyp-equipment_insect_monitor', 'glyp-equipment_lethal_animal_trap', 'glyp-equipment_live_animal_trap', 'glyp-equipment_live_rodent_trap', 'glyp-equipment_maintenance', 'glyp-equipment_map_viewer', 'glyp-equipment_maps', 'glyp-equipment_mosquito_trap', 'glyp-equipment_other', 'glyp-equipment_pheromone_trap', 'glyp-equipment_pick_up', 'glyp-equipment_rodent_bait', 'glyp-equipment_rodent_trap', 'glyp-equipment_termite_bait_station', 'glyp-equipment_termite_monitor', 'glyp-exclusion', 'glyp-expand', 'glyp-expiring', 'glyp-exterior', 'glyp-extras', 'glyp-facebook', 'glyp-farm', 'glyp-farm_grain_seed', 'glyp-fast_and_frequent', 'glyp-favorite', 'glyp-feedback', 'glyp-field_guide', 'glyp-field_training', 'glyp-file_blank', 'glyp-file_html', 'glyp-file_image', 'glyp-file_pdf', 'glyp-file_printable', 'glyp-file_spreadsheet', 'glyp-file_text', 'glyp-filter', 'glyp-finance', 'glyp-finding', 'glyp-folder', 'glyp-followup', 'glyp-food_bev_pharma', 'glyp-formula', 'glyp-fuel', 'glyp-fumigation', 'glyp-function', 'glyp-function_aggregate', 'glyp-function_time', 'glyp-gain_loss', 'glyp-generate_invoices', 'glyp-generate_orders', 'glyp-geo', 'glyp-geo_heat_map', 'glyp-google', 'glyp-google_calendar', 'glyp-government', 'glyp-grain_seed', 'glyp-grid', 'glyp-grouped', 'glyp-grouping_combine', 'glyp-grouping_separate', 'glyp-has_many', 'glyp-health_care', 'glyp-heat_map', 'glyp-heat_treatment', 'glyp-help', 'glyp-hidden', 'glyp-honeybee_removal', 'glyp-housing', 'glyp-in_progress', 'glyp-incomplete_certificate', 'glyp-industrial', 'glyp-info', 'glyp-information_technology', 'glyp-inspect_map', 'glyp-inspections', 'glyp-insulation', 'glyp-interior', 'glyp-invoice', 'glyp-invoice_review', 'glyp-irrigation_install', 'glyp-irrigation_maintenance', 'glyp-items', 'glyp-job', 'glyp-join', 'glyp-join_inner', 'glyp-join_left', 'glyp-justice', 'glyp-k9_inspection', 'glyp-key', 'glyp-label_bottom', 'glyp-label_left', 'glyp-label_right', 'glyp-label_top', 'glyp-labor', 'glyp-landscape', 'glyp-landscape_maintenance', 'glyp-laptop', 'glyp-lead', 'glyp-lead_listing', 'glyp-lead_source', 'glyp-leads_report', 'glyp-learning_hub', 'glyp-ledger', 'glyp-legal', 'glyp-license', 'glyp-lifetime', 'glyp-line_cap_arrow', 'glyp-line_cap_bar', 'glyp-line_cap_circle', 'glyp-line_cap_none', 'glyp-line_caps', 'glyp-line_style', 'glyp-link', 'glyp-location', 'glyp-location_charge', 'glyp-location_credit', 'glyp-location_kind', 'glyp-location_message', 'glyp-location_origin', 'glyp-location_tags', 'glyp-locations', 'glyp-locked', 'glyp-lodging', 'glyp-log_in', 'glyp-log_out', 'glyp-log_report', 'glyp-logbook', 'glyp-logbook_documents', 'glyp-lot_number', 'glyp-manager', 'glyp-map', 'glyp-markdown', 'glyp-market', 'glyp-materials', 'glyp-mattress_encasement', 'glyp-merge', 'glyp-message_billing', 'glyp-message_collections', 'glyp-message_complaint', 'glyp-message_misc', 'glyp-message_technician', 'glyp-messages', 'glyp-misc', 'glyp-miscellaneous', 'glyp-move_order', 'glyp-mowing', 'glyp-multi_housing', 'glyp-mute', 'glyp-navicon', 'glyp-new_location', 'glyp-no_charge', 'glyp-no_service', 'glyp-note', 'glyp-note_access', 'glyp-note_billing', 'glyp-note_collection', 'glyp-note_complaint', 'glyp-note_directions', 'glyp-note_focused', 'glyp-note_office', 'glyp-note_preservice', 'glyp-note_sales', 'glyp-note_service', 'glyp-notification', 'glyp-number', 'glyp-office', 'glyp-office_government', 'glyp-office_training', 'glyp-on_demand', 'glyp-on_demand_order', 'glyp-open', 'glyp-open_invoice', 'glyp-operations', 'glyp-options', 'glyp-order_actions', 'glyp-order_approved', 'glyp-order_batch', 'glyp-order_editor', 'glyp-order_pending', 'glyp-order_series', 'glyp-order_unapproved', 'glyp-org_structure', 'glyp-org_unit', 'glyp-org_unit_settings', 'glyp-origin', 'glyp-origin_client', 'glyp-origin_tech', 'glyp-outlook', 'glyp-overlap', 'glyp-password', 'glyp-past_due', 'glyp-paused', 'glyp-pay_brackets', 'glyp-payment', 'glyp-payment_ach', 'glyp-payment_application_apply', 'glyp-payment_application_correction', 'glyp-payment_application_initial', 'glyp-payment_application_refund', 'glyp-payment_application_transfer', 'glyp-payment_application_unapply', 'glyp-payment_application_unapply_all', 'glyp-payment_applications', 'glyp-payment_batch', 'glyp-payment_cash', 'glyp-payment_check', 'glyp-payment_coupon', 'glyp-payment_credit', 'glyp-payment_discount', 'glyp-payment_eft', 'glyp-payment_finance_charge', 'glyp-payments', 'glyp-payroll', 'glyp-pdf', 'glyp-pending', 'glyp-periodic', 'glyp-periodic_assessment', 'glyp-permission', 'glyp-pest', 'glyp-pest_ants', 'glyp-pest_bats', 'glyp-pest_bed_bugs', 'glyp-pest_birds', 'glyp-pest_crawling_insects', 'glyp-pest_dermestids', 'glyp-pest_fall_invaders', 'glyp-pest_flying_insects', 'glyp-pest_moles', 'glyp-pest_mosquitoes', 'glyp-pest_nuisance_animals', 'glyp-pest_ornamental', 'glyp-pest_ornamental_diseases', 'glyp-pest_roaches', 'glyp-pest_rodents', 'glyp-pest_scorpions', 'glyp-pest_snakes', 'glyp-pest_spiders', 'glyp-pest_stinging_insects', 'glyp-pest_stored_product_pests', 'glyp-pest_ticks_and_fleas', 'glyp-pest_viruses_and_bacteria', 'glyp-pest_weeds', 'glyp-pest_wood_destroyers', 'glyp-phone', 'glyp-pick_date', 'glyp-pick_list', 'glyp-platform_android', 'glyp-platform_ios', 'glyp-platform_macos', 'glyp-platform_windows', 'glyp-play', 'glyp-plus', 'glyp-plus_filled', 'glyp-plus_outline', 'glyp-portal', 'glyp-price_increase', 'glyp-price_increase_alt', 'glyp-pricing_table', 'glyp-print', 'glyp-privacy', 'glyp-product_sale', 'glyp-production', 'glyp-professional_consultation', 'glyp-program', 'glyp-program_elements', 'glyp-program_initiation', 'glyp-program_order', 'glyp-program_review', 'glyp-promo', 'glyp-proposal', 'glyp-protection', 'glyp-purchase_order', 'glyp-quality', 'glyp-query', 'glyp-radio_checked', 'glyp-radio_empty', 'glyp-reassign', 'glyp-receipt', 'glyp-recent', 'glyp-recommendation', 'glyp-record_details', 'glyp-recurring_revenue', 'glyp-redo', 'glyp-refresh', 'glyp-refund', 'glyp-region', 'glyp-released', 'glyp-remove', 'glyp-renewal', 'glyp-report', 'glyp-required_input', 'glyp-reschedule', 'glyp-restaurant_bar', 'glyp-revenue', 'glyp-review', 'glyp-rfid', 'glyp-ride_along', 'glyp-rodent_exclusion', 'glyp-route_change', 'glyp-route_optimization', 'glyp-rows', 'glyp-ruler', 'glyp-sales', 'glyp-sales_contest', 'glyp-sales_pipeline', 'glyp-sales_tax', 'glyp-sales_tax_exemption', 'glyp-schedule_lock', 'glyp-schedule_lock_afternoon', 'glyp-schedule_lock_day', 'glyp-schedule_lock_exact', 'glyp-schedule_lock_four_hour', 'glyp-schedule_lock_morning', 'glyp-schedule_lock_none', 'glyp-schedule_lock_two_day', 'glyp-schedule_lock_two_hour', 'glyp-schedule_lock_week', 'glyp-schedule_tyoe_every', 'glyp-schedule_type_every', 'glyp-schedule_type_none', 'glyp-schedule_type_on_demand', 'glyp-schedule_type_schedule', 'glyp-scheduled', 'glyp-scheduling', 'glyp-school_church', 'glyp-script', 'glyp-search', 'glyp-seeding', 'glyp-segment', 'glyp-sent', 'glyp-sentricon', 'glyp-service_agreement', 'glyp-service_form', 'glyp-service_miscellaneous', 'glyp-service_reminder', 'glyp-service_report', 'glyp-service_request', 'glyp-services', 'glyp-settings', 'glyp-setup', 'glyp-signature', 'glyp-simulator', 'glyp-site_map', 'glyp-site_map_annotation', 'glyp-site_map_chemical', 'glyp-site_map_edit_details', 'glyp-site_map_edit_geo', 'glyp-site_map_equipment', 'glyp-site_map_exterior', 'glyp-site_map_foundation', 'glyp-site_map_icon', 'glyp-site_map_interior', 'glyp-site_map_label', 'glyp-site_map_perimeter', 'glyp-site_map_settings_left', 'glyp-site_map_settings_right', 'glyp-site_map_template', 'glyp-skip', 'glyp-smart_traps', 'glyp-sms', 'glyp-snow_removal', 'glyp-sort', 'glyp-special', 'glyp-split', 'glyp-spp_scorecard', 'glyp-square_edge', 'glyp-start_sheet', 'glyp-start_time', 'glyp-statement', 'glyp-states', 'glyp-sticky', 'glyp-stop_time', 'glyp-store_material', 'glyp-store_order', 'glyp-store_order_back_order', 'glyp-store_order_cancelled', 'glyp-store_order_complete', 'glyp-store_order_pending', 'glyp-store_order_pending_return', 'glyp-store_order_pre_order', 'glyp-store_order_returned', 'glyp-stores', 'glyp-styling', 'glyp-subscribe', 'glyp-supplemental', 'glyp-support', 'glyp-sync', 'glyp-table', 'glyp-tablet', 'glyp-tablets', 'glyp-tag_manager', 'glyp-tags', 'glyp-task_runs', 'glyp-tech_pest_sightings', 'glyp-technician', 'glyp-technician_approach', 'glyp-template', 'glyp-termite_fumigation', 'glyp-terrier', 'glyp-territory', 'glyp-text', 'glyp-thermometer', 'glyp-third_party_billing', 'glyp-ticket_type', 'glyp-tickets', 'glyp-tidy', 'glyp-time', 'glyp-time_entry', 'glyp-timeline', 'glyp-toolbox', 'glyp-tqi', 'glyp-tracker', 'glyp-tracking', 'glyp-training_course', 'glyp-treatment_split', 'glyp-trends', 'glyp-trip_charge', 'glyp-ui_type', 'glyp-unchecked', 'glyp-undo', 'glyp-unit_sweep', 'glyp-unit_tag', 'glyp-university', 'glyp-unlocked', 'glyp-unscheduled', 'glyp-update_check_positions', 'glyp-upload', 'glyp-user', 'glyp-user_credit', 'glyp-user_tags', 'glyp-users', 'glyp-utility', 'glyp-vacation', 'glyp-vacuum', 'glyp-variant', 'glyp-vendor', 'glyp-versions', 'glyp-video', 'glyp-visible', 'glyp-warehouse', 'glyp-wdo', 'glyp-web_dusting', 'glyp-website', 'glyp-wildlife', 'glyp-wildlife_exclusion', 'glyp-winter_check', 'glyp-wizard', 'glyp-work', 'glyp-work_class', 'glyp-work_code', 'glyp-work_order', 'glyp-work_production', 'glyp-work_progress', 'glyp-work_yield', 'glyp-year', 'glyp-yield', 'glyp-zip_code', 'glyp-zone_adjacent', 'glyp-zone_check', 'glyp-zone_production', 'glyp-zone_remote', 'glyp-zoom_fit'] as const
3
+ const names = ['glyp-abacus', 'glyp-account', 'glyp-accounts_payable', 'glyp-accounts_receivable', 'glyp-action_log', 'glyp-activate', 'glyp-active', 'glyp-activity_high', 'glyp-activity_low', 'glyp-activity_medium', 'glyp-activity_none', 'glyp-address1', 'glyp-adjustment', 'glyp-admin', 'glyp-administration', 'glyp-air_freshener', 'glyp-alert', 'glyp-align_horizontal', 'glyp-align_vertical', 'glyp-anchor', 'glyp-announcement', 'glyp-app_update', 'glyp-append_selection', 'glyp-appointment', 'glyp-appointment_setup', 'glyp-archive', 'glyp-arrow_down', 'glyp-arrow_left', 'glyp-arrow_right', 'glyp-arrow_transfer', 'glyp-arrow_up', 'glyp-ascending', 'glyp-attachment', 'glyp-audit_analyzer', 'glyp-autopay', 'glyp-availability', 'glyp-background_batch', 'glyp-background_job', 'glyp-bait_for_demolition', 'glyp-bar_chart', 'glyp-barcode', 'glyp-bat_exclusion', 'glyp-bed_bug_fumigation', 'glyp-begin_service', 'glyp-belongs_to', 'glyp-billing', 'glyp-billing_office', 'glyp-billing_terms', 'glyp-billto', 'glyp-bioremediation', 'glyp-bird_exclusion', 'glyp-black_light', 'glyp-branch', 'glyp-branch_bats', 'glyp-branch_birds', 'glyp-branch_general', 'glyp-branch_irrigation', 'glyp-branch_landscape', 'glyp-branch_multi_housing', 'glyp-branch_office', 'glyp-branch_orders', 'glyp-branch_residential', 'glyp-branch_sales', 'glyp-branch_termites', 'glyp-branch_weeds', 'glyp-branch_wildlife', 'glyp-build', 'glyp-calculate', 'glyp-calendar', 'glyp-calendar_branch', 'glyp-calendar_map', 'glyp-calendar_rolling', 'glyp-calendar_share', 'glyp-calendar_snap', 'glyp-california', 'glyp-call', 'glyp-callback', 'glyp-camera', 'glyp-cancellation', 'glyp-cancelled', 'glyp-card_american_express', 'glyp-card_discover', 'glyp-card_mastercard', 'glyp-card_visa', 'glyp-catalog', 'glyp-caught', 'glyp-cert', 'glyp-check_all', 'glyp-check_in', 'glyp-check_in_service', 'glyp-checked', 'glyp-checked_empty', 'glyp-checkmark', 'glyp-checkmark_filled', 'glyp-checkout', 'glyp-chemical', 'glyp-chemical_authorization', 'glyp-chemical_backpack_sprayer', 'glyp-chemical_biological_controls', 'glyp-chemical_disinfectant', 'glyp-chemical_fertilizer', 'glyp-chemical_flying_insect_bait', 'glyp-chemical_fumigants', 'glyp-chemical_insect_aerosol', 'glyp-chemical_insect_bait', 'glyp-chemical_insect_dust', 'glyp-chemical_insect_granules', 'glyp-chemical_insect_spray', 'glyp-chemical_label', 'glyp-chemical_mole_bait', 'glyp-chemical_pest_bird_control', 'glyp-chemical_pheromone_products', 'glyp-chemical_power_sprayer', 'glyp-chemical_rodenticide_block', 'glyp-chemical_rodenticide_non_block', 'glyp-chemical_sds', 'glyp-chemical_tree_products', 'glyp-chemical_weed_control', 'glyp-chemical_wildlife_repellents', 'glyp-chemical_wildlife_toxicants', 'glyp-chevron_down', 'glyp-chevron_left', 'glyp-chevron_right', 'glyp-chevron_up', 'glyp-city', 'glyp-click', 'glyp-client_actions', 'glyp-client_pest_sightings', 'glyp-client_required', 'glyp-close', 'glyp-cloud', 'glyp-clypboard', 'glyp-clypedia', 'glyp-clypmart', 'glyp-code', 'glyp-code_details', 'glyp-collections', 'glyp-columns', 'glyp-comment', 'glyp-comment_filled', 'glyp-commission_origin', 'glyp-commissions', 'glyp-company_setup', 'glyp-compass', 'glyp-complete', 'glyp-complete_certificate', 'glyp-complete_enrollment', 'glyp-condition_appliance_machinery', 'glyp-condition_customer', 'glyp-condition_device', 'glyp-condition_door_window', 'glyp-condition_dumpster', 'glyp-condition_entry_point', 'glyp-condition_evidence', 'glyp-condition_exclusion', 'glyp-condition_exterior', 'glyp-condition_interior', 'glyp-condition_plumbing', 'glyp-condition_prep_storage', 'glyp-condition_rodent_evidence', 'glyp-condition_rodent_exclusion', 'glyp-condition_roof_ceiling', 'glyp-condition_structure', 'glyp-condition_supplies', 'glyp-condition_termites', 'glyp-condition_ventilation', 'glyp-condition_wall_floor', 'glyp-condition_wildlife', 'glyp-conditions', 'glyp-consolidate', 'glyp-construction', 'glyp-contract', 'glyp-contract_cancellation', 'glyp-contract_overview', 'glyp-conversation', 'glyp-copesan', 'glyp-copy', 'glyp-credit_memo', 'glyp-credit_non_revenue', 'glyp-credit_production', 'glyp-credit_prospect', 'glyp-credit_sales_bonus', 'glyp-crew', 'glyp-cumulative', 'glyp-cursor', 'glyp-custom_form', 'glyp-customer', 'glyp-customer_service', 'glyp-dashboard', 'glyp-data_dive', 'glyp-data_dive_query', 'glyp-data_dives', 'glyp-database', 'glyp-dead_animal_removal', 'glyp-default', 'glyp-delete', 'glyp-demo_mode', 'glyp-descending', 'glyp-design', 'glyp-desktop', 'glyp-detail_report', 'glyp-developer', 'glyp-device', 'glyp-device_sync', 'glyp-differential', 'glyp-disable', 'glyp-disabled_filled', 'glyp-distribute_horizontal', 'glyp-distribute_vertical', 'glyp-division', 'glyp-document', 'glyp-documentation', 'glyp-documents', 'glyp-download', 'glyp-driving', 'glyp-duplicate', 'glyp-duplicate_location', 'glyp-duration', 'glyp-edit', 'glyp-email', 'glyp-employment', 'glyp-entertainment_public_venues', 'glyp-equipment_bear_trap', 'glyp-equipment_flying_insect_bait_station', 'glyp-equipment_flying_insect_light_trap', 'glyp-equipment_insect_monitor', 'glyp-equipment_lethal_animal_trap', 'glyp-equipment_live_animal_trap', 'glyp-equipment_live_rodent_trap', 'glyp-equipment_maintenance', 'glyp-equipment_map_viewer', 'glyp-equipment_maps', 'glyp-equipment_mosquito_trap', 'glyp-equipment_other', 'glyp-equipment_pheromone_trap', 'glyp-equipment_pick_up', 'glyp-equipment_rodent_bait', 'glyp-equipment_rodent_trap', 'glyp-equipment_termite_bait_station', 'glyp-equipment_termite_monitor', 'glyp-exclusion', 'glyp-expand', 'glyp-expiring', 'glyp-exterior', 'glyp-extras', 'glyp-facebook', 'glyp-farm', 'glyp-farm_grain_seed', 'glyp-fast_and_frequent', 'glyp-favorite', 'glyp-feedback', 'glyp-field_guide', 'glyp-field_training', 'glyp-file_blank', 'glyp-file_html', 'glyp-file_image', 'glyp-file_pdf', 'glyp-file_printable', 'glyp-file_spreadsheet', 'glyp-file_text', 'glyp-filter', 'glyp-finance', 'glyp-finding', 'glyp-folder', 'glyp-followup', 'glyp-food_bev_pharma', 'glyp-formula', 'glyp-fuel', 'glyp-fumigation', 'glyp-function', 'glyp-function_aggregate', 'glyp-function_time', 'glyp-gain_loss', 'glyp-generate_invoices', 'glyp-generate_orders', 'glyp-geo', 'glyp-geo_heat_map', 'glyp-google', 'glyp-google_calendar', 'glyp-government', 'glyp-grain_seed', 'glyp-grid', 'glyp-grouped', 'glyp-grouping_combine', 'glyp-grouping_separate', 'glyp-has_many', 'glyp-health_care', 'glyp-heat_map', 'glyp-heat_treatment', 'glyp-help', 'glyp-hidden', 'glyp-hint', 'glyp-honeybee_removal', 'glyp-housing', 'glyp-in_progress', 'glyp-incomplete_certificate', 'glyp-industrial', 'glyp-info', 'glyp-information_technology', 'glyp-inspect_map', 'glyp-inspections', 'glyp-insulation', 'glyp-interior', 'glyp-invoice', 'glyp-invoice_review', 'glyp-irrigation_install', 'glyp-irrigation_maintenance', 'glyp-items', 'glyp-job', 'glyp-join', 'glyp-join_inner', 'glyp-join_left', 'glyp-justice', 'glyp-k9_inspection', 'glyp-key', 'glyp-label_bottom', 'glyp-label_left', 'glyp-label_right', 'glyp-label_top', 'glyp-labor', 'glyp-landscape', 'glyp-landscape_maintenance', 'glyp-laptop', 'glyp-lead', 'glyp-lead_listing', 'glyp-lead_source', 'glyp-leads_report', 'glyp-learning_hub', 'glyp-ledger', 'glyp-legal', 'glyp-license', 'glyp-lifetime', 'glyp-line_cap_arrow', 'glyp-line_cap_bar', 'glyp-line_cap_circle', 'glyp-line_cap_none', 'glyp-line_caps', 'glyp-line_style', 'glyp-link', 'glyp-location', 'glyp-location_charge', 'glyp-location_credit', 'glyp-location_kind', 'glyp-location_message', 'glyp-location_origin', 'glyp-location_tags', 'glyp-locations', 'glyp-locked', 'glyp-lodging', 'glyp-log_in', 'glyp-log_out', 'glyp-log_report', 'glyp-logbook', 'glyp-logbook_documents', 'glyp-lot_number', 'glyp-manager', 'glyp-map', 'glyp-markdown', 'glyp-market', 'glyp-materials', 'glyp-mattress_encasement', 'glyp-merge', 'glyp-message_billing', 'glyp-message_collections', 'glyp-message_complaint', 'glyp-message_misc', 'glyp-message_technician', 'glyp-messages', 'glyp-misc', 'glyp-miscellaneous', 'glyp-move_order', 'glyp-mowing', 'glyp-multi_housing', 'glyp-mute', 'glyp-navicon', 'glyp-new_location', 'glyp-no_charge', 'glyp-no_service', 'glyp-note', 'glyp-note_access', 'glyp-note_billing', 'glyp-note_collection', 'glyp-note_complaint', 'glyp-note_directions', 'glyp-note_focused', 'glyp-note_office', 'glyp-note_preservice', 'glyp-note_sales', 'glyp-note_service', 'glyp-notification', 'glyp-number', 'glyp-office', 'glyp-office_government', 'glyp-office_training', 'glyp-on_demand', 'glyp-on_demand_order', 'glyp-open', 'glyp-open_invoice', 'glyp-operations', 'glyp-options', 'glyp-order_actions', 'glyp-order_approved', 'glyp-order_batch', 'glyp-order_editor', 'glyp-order_pending', 'glyp-order_series', 'glyp-order_unapproved', 'glyp-org_structure', 'glyp-org_unit', 'glyp-org_unit_settings', 'glyp-origin', 'glyp-origin_client', 'glyp-origin_tech', 'glyp-outlook', 'glyp-overlap', 'glyp-password', 'glyp-past_due', 'glyp-paused', 'glyp-pay_brackets', 'glyp-payment', 'glyp-payment_ach', 'glyp-payment_application_apply', 'glyp-payment_application_correction', 'glyp-payment_application_initial', 'glyp-payment_application_refund', 'glyp-payment_application_transfer', 'glyp-payment_application_unapply', 'glyp-payment_application_unapply_all', 'glyp-payment_applications', 'glyp-payment_batch', 'glyp-payment_cash', 'glyp-payment_check', 'glyp-payment_coupon', 'glyp-payment_credit', 'glyp-payment_discount', 'glyp-payment_eft', 'glyp-payment_finance_charge', 'glyp-payments', 'glyp-payroll', 'glyp-pdf', 'glyp-pending', 'glyp-periodic', 'glyp-periodic_assessment', 'glyp-permission', 'glyp-pest', 'glyp-pest_ants', 'glyp-pest_bats', 'glyp-pest_bed_bugs', 'glyp-pest_birds', 'glyp-pest_crawling_insects', 'glyp-pest_dermestids', 'glyp-pest_fall_invaders', 'glyp-pest_flying_insects', 'glyp-pest_moles', 'glyp-pest_mosquitoes', 'glyp-pest_nuisance_animals', 'glyp-pest_ornamental', 'glyp-pest_ornamental_diseases', 'glyp-pest_roaches', 'glyp-pest_rodents', 'glyp-pest_scorpions', 'glyp-pest_snakes', 'glyp-pest_spiders', 'glyp-pest_stinging_insects', 'glyp-pest_stored_product_pests', 'glyp-pest_ticks_and_fleas', 'glyp-pest_viruses_and_bacteria', 'glyp-pest_weeds', 'glyp-pest_wood_destroyers', 'glyp-phone', 'glyp-pick_date', 'glyp-pick_list', 'glyp-platform_android', 'glyp-platform_ios', 'glyp-platform_macos', 'glyp-platform_windows', 'glyp-play', 'glyp-plus', 'glyp-plus_filled', 'glyp-plus_outline', 'glyp-portal', 'glyp-price_increase', 'glyp-price_increase_alt', 'glyp-pricing_table', 'glyp-print', 'glyp-privacy', 'glyp-product_sale', 'glyp-production', 'glyp-professional_consultation', 'glyp-program', 'glyp-program_elements', 'glyp-program_initiation', 'glyp-program_order', 'glyp-program_review', 'glyp-promo', 'glyp-proposal', 'glyp-protection', 'glyp-purchase_order', 'glyp-quality', 'glyp-query', 'glyp-radio_checked', 'glyp-radio_empty', 'glyp-reassign', 'glyp-receipt', 'glyp-recent', 'glyp-recommendation', 'glyp-record_details', 'glyp-recurring_revenue', 'glyp-redo', 'glyp-refresh', 'glyp-refund', 'glyp-region', 'glyp-released', 'glyp-remove', 'glyp-renewal', 'glyp-report', 'glyp-required_input', 'glyp-reschedule', 'glyp-restaurant_bar', 'glyp-revenue', 'glyp-review', 'glyp-rfid', 'glyp-ride_along', 'glyp-rodent_exclusion', 'glyp-route_change', 'glyp-route_optimization', 'glyp-rows', 'glyp-ruler', 'glyp-sales', 'glyp-sales_contest', 'glyp-sales_pipeline', 'glyp-sales_tax', 'glyp-sales_tax_exemption', 'glyp-schedule_lock', 'glyp-schedule_lock_afternoon', 'glyp-schedule_lock_day', 'glyp-schedule_lock_exact', 'glyp-schedule_lock_four_hour', 'glyp-schedule_lock_morning', 'glyp-schedule_lock_none', 'glyp-schedule_lock_two_day', 'glyp-schedule_lock_two_hour', 'glyp-schedule_lock_week', 'glyp-schedule_tyoe_every', 'glyp-schedule_type_every', 'glyp-schedule_type_none', 'glyp-schedule_type_on_demand', 'glyp-schedule_type_schedule', 'glyp-scheduled', 'glyp-scheduling', 'glyp-school_church', 'glyp-script', 'glyp-search', 'glyp-seeding', 'glyp-segment', 'glyp-sent', 'glyp-sentricon', 'glyp-service_agreement', 'glyp-service_form', 'glyp-service_miscellaneous', 'glyp-service_reminder', 'glyp-service_report', 'glyp-service_request', 'glyp-services', 'glyp-settings', 'glyp-setup', 'glyp-signature', 'glyp-simulator', 'glyp-site_map', 'glyp-site_map_annotation', 'glyp-site_map_chemical', 'glyp-site_map_edit_details', 'glyp-site_map_edit_geo', 'glyp-site_map_equipment', 'glyp-site_map_exterior', 'glyp-site_map_foundation', 'glyp-site_map_icon', 'glyp-site_map_interior', 'glyp-site_map_label', 'glyp-site_map_perimeter', 'glyp-site_map_settings_left', 'glyp-site_map_settings_right', 'glyp-site_map_template', 'glyp-skip', 'glyp-smart_traps', 'glyp-sms', 'glyp-snow_removal', 'glyp-sort', 'glyp-special', 'glyp-split', 'glyp-spp_scorecard', 'glyp-square_edge', 'glyp-start_sheet', 'glyp-start_time', 'glyp-statement', 'glyp-states', 'glyp-sticky', 'glyp-stop_time', 'glyp-store_material', 'glyp-store_order', 'glyp-store_order_back_order', 'glyp-store_order_cancelled', 'glyp-store_order_complete', 'glyp-store_order_pending', 'glyp-store_order_pending_return', 'glyp-store_order_pre_order', 'glyp-store_order_returned', 'glyp-stores', 'glyp-styling', 'glyp-subscribe', 'glyp-supplemental', 'glyp-support', 'glyp-sync', 'glyp-table', 'glyp-tablet', 'glyp-tablets', 'glyp-tag_manager', 'glyp-tags', 'glyp-task_runs', 'glyp-tech_pest_sightings', 'glyp-technician', 'glyp-technician_approach', 'glyp-template', 'glyp-termite_fumigation', 'glyp-terrier', 'glyp-territory', 'glyp-text', 'glyp-thermometer', 'glyp-third_party_billing', 'glyp-ticket_type', 'glyp-tickets', 'glyp-tidy', 'glyp-time', 'glyp-time_entry', 'glyp-timeline', 'glyp-toolbox', 'glyp-tqi', 'glyp-tracker', 'glyp-tracking', 'glyp-training_course', 'glyp-treatment_split', 'glyp-trends', 'glyp-trip_charge', 'glyp-ui_type', 'glyp-unchecked', 'glyp-undo', 'glyp-unit_sweep', 'glyp-unit_tag', 'glyp-university', 'glyp-unlocked', 'glyp-unscheduled', 'glyp-update_check_positions', 'glyp-upload', 'glyp-user', 'glyp-user_credit', 'glyp-user_tags', 'glyp-users', 'glyp-utility', 'glyp-vacation', 'glyp-vacuum', 'glyp-variant', 'glyp-vendor', 'glyp-versions', 'glyp-video', 'glyp-visible', 'glyp-warehouse', 'glyp-wdo', 'glyp-web_dusting', 'glyp-website', 'glyp-wildlife', 'glyp-wildlife_exclusion', 'glyp-winter_check', 'glyp-wizard', 'glyp-work', 'glyp-work_class', 'glyp-work_code', 'glyp-work_order', 'glyp-work_production', 'glyp-work_progress', 'glyp-work_yield', 'glyp-year', 'glyp-yield', 'glyp-zip_code', 'glyp-zone_adjacent', 'glyp-zone_check', 'glyp-zone_production', 'glyp-zone_remote', 'glyp-zoom_fit'] as const
4
4
 
5
5
  const Glyps = {
6
6
  names
@@ -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() {
@@ -1,10 +1,9 @@
1
- import {Action, RenderActionOptions} from "../theme"
1
+ import {Action, IconName, RenderActionOptions} from "../theme"
2
2
  import ContentPart, {ActionLevel} from "./content-part"
3
3
  import {PartTag} from "tuff-core/parts"
4
4
  import {optionsForSelect, SelectOptions} from "tuff-core/forms"
5
5
  import {UntypedKey} from "tuff-core/messages"
6
6
  import {Logger} from "tuff-core/logging"
7
- import {HtmlParentTag} from "tuff-core/html"
8
7
 
9
8
  const log = new Logger("Terrier PagePart")
10
9
 
@@ -18,15 +17,17 @@ export type ContentWidth = "normal" | "wide"
18
17
  type BaseFieldDef = { name: string } & ToolbarFieldDefOptions
19
18
 
20
19
  type ToolbarFieldDefOptions = {
21
- onChangeKey?: UntypedKey,
22
- onInputKey?: UntypedKey,
23
- defaultValue?: string,
20
+ onChangeKey?: UntypedKey
21
+ onInputKey?: UntypedKey
22
+ defaultValue?: string
24
23
  tooltip?: string
24
+ title?: string
25
+ icon?: IconName
25
26
  }
26
27
 
27
28
  type ToolbarSelectDef = { type: 'select', options: SelectOptions } & BaseFieldDef
28
29
 
29
- type ValuedInputType = 'text' | 'color' | 'date' | 'datetime-local' | 'email' | 'hidden' | 'month' | 'number' | 'password' | 'search' | 'tel' | 'time' | 'url' | 'week'
30
+ type ValuedInputType = 'text' | 'color' | 'date' | 'datetime-local' | 'email' | 'hidden' | 'month' | 'number' | 'password' | 'search' | 'tel' | 'time' | 'url' | 'week' | 'checkbox'
30
31
  type ToolbarValuedInputDef = { type: ValuedInputType } & BaseFieldDef
31
32
 
32
33
  /**
@@ -163,7 +164,7 @@ export default abstract class PagePart<TState> extends ContentPart<TState> {
163
164
  }
164
165
 
165
166
  protected renderToolbarFields(parent: PartTag) {
166
- parent.div('.fields.tt-flex.align-center.small-gap', fields => {
167
+ parent.div('.fields', fields => {
167
168
  for (const name of this._toolbarFieldsOrder) {
168
169
  const def = this._toolbarFields[name]
169
170
  if (!def) {
@@ -171,18 +172,33 @@ export default abstract class PagePart<TState> extends ContentPart<TState> {
171
172
  continue;
172
173
  }
173
174
 
174
- let field!: HtmlParentTag
175
- if (def.type === 'select') {
176
- field = fields.select({name: def.name}, select => {
177
- optionsForSelect(select, def.options, def.defaultValue)
178
- })
179
- } else {
180
- field = fields.input({name: def.name, type: def.type})
181
- }
182
-
183
- if (def.onChangeKey) field.emitChange(def.onChangeKey)
184
- if (def.onInputKey) field.emitInput(def.onInputKey)
185
- if (def.tooltip?.length) field.dataAttr('tooltip', def.tooltip)
175
+ fields.label(label => {
176
+ if (def.icon?.length) {
177
+ label.i('.icon').class(def.icon)
178
+ }
179
+
180
+ if (def.title?.length) {
181
+ label.div('.title').text(def.title)
182
+ }
183
+
184
+ if (def.type === 'select') {
185
+ const select = label.select({name: def.name}, select => {
186
+ optionsForSelect(select, def.options, def.defaultValue)
187
+ })
188
+ if (def.onChangeKey) select.emitChange(def.onChangeKey)
189
+ if (def.onInputKey) select.emitInput(def.onInputKey)
190
+ } else {
191
+ const input = label.input({name: def.name, type: def.type, value: def.defaultValue})
192
+ if (def.type == 'checkbox' && def.defaultValue?.length) {
193
+ input.attrs({checked: def.defaultValue == 'true'})
194
+ }
195
+ if (def.onChangeKey) input.emitChange(def.onChangeKey)
196
+ if (def.onInputKey) input.emitInput(def.onInputKey)
197
+ }
198
+
199
+
200
+ if (def.tooltip?.length) label.dataAttr('tooltip', def.tooltip)
201
+ })
186
202
  }
187
203
  })
188
204
  }
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/tabs.ts CHANGED
@@ -117,17 +117,17 @@ export class TabContainerPart extends TerrierPart<TabContainerState> {
117
117
  }
118
118
 
119
119
 
120
- _beforeAction?: Action
120
+ _beforeActions: Action[] = []
121
121
 
122
- setBeforeAction(action: Action) {
123
- this._beforeAction = action
122
+ addBeforeAction(action: Action) {
123
+ this._beforeActions.push(action)
124
124
  this.dirty()
125
125
  }
126
126
 
127
- _afterAction?: Action
127
+ _afterActions: Action[] = []
128
128
 
129
- setAfterAction(action: Action) {
130
- this._afterAction = action
129
+ addAfterAction(action: Action) {
130
+ this._afterActions.push(action)
131
131
  this.dirty()
132
132
  }
133
133
 
@@ -141,8 +141,8 @@ export class TabContainerPart extends TerrierPart<TabContainerState> {
141
141
  }
142
142
  parent.div('tt-tab-container', this.state.side, container => {
143
143
  container.div('.tt-flex.tt-tab-list', tabList => {
144
- if (this._beforeAction) {
145
- this.theme.renderActions(tabList, [this._beforeAction], {defaultClass: 'action'})
144
+ if (this._beforeActions.length) {
145
+ this.theme.renderActions(tabList, this._beforeActions, {defaultClass: 'action'})
146
146
  }
147
147
  for (const tab of Object.values(this.tabs)) {
148
148
  if (tab.state == 'hidden') continue
@@ -162,9 +162,9 @@ export class TabContainerPart extends TerrierPart<TabContainerState> {
162
162
  }
163
163
  })
164
164
  }
165
- if (this._afterAction) {
165
+ if (this._afterActions.length) {
166
166
  tabList.div('.spacer')
167
- this.theme.renderActions(tabList, [this._afterAction], {defaultClass: 'action'})
167
+ this.theme.renderActions(tabList, this._afterActions, {defaultClass: 'action'})
168
168
  }
169
169
  })
170
170
 
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
  }