terrier-engine 4.4.19 → 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.
- package/data-dive/dives/dive-editor.ts +25 -2
- package/data-dive/queries/columns.ts +49 -0
- package/data-dive/queries/filters.ts +46 -14
- package/data-dive/queries/tables.ts +29 -8
- package/package.json +1 -1
- package/terrier/dropdowns.ts +9 -3
- package/terrier/gen/hub-icons.ts +9 -1
- package/terrier/images/icons/read_mail.svg +1 -0
- package/terrier/images/optimized/icon-read_mail.svg +1 -0
- package/terrier/images/raw/icon-read_mail.svg +21 -0
- package/terrier/overlays.ts +42 -6
- package/terrier/parts/content-part.ts +4 -5
- package/terrier/schema.ts +8 -4
- package/terrier/theme.ts +4 -0
|
@@ -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-
|
|
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
|
|
|
@@ -91,6 +92,7 @@ export type ColumnsEditorState = {
|
|
|
91
92
|
|
|
92
93
|
const saveKey = messages.untypedKey()
|
|
93
94
|
const addKey = messages.untypedKey()
|
|
95
|
+
const addSingleKey = messages.typedKey<{ name: string }>()
|
|
94
96
|
const removeKey = messages.typedKey<{id: string}>()
|
|
95
97
|
|
|
96
98
|
/**
|
|
@@ -151,6 +153,19 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
151
153
|
this.removeColumn(m.data.id)
|
|
152
154
|
})
|
|
153
155
|
|
|
156
|
+
this.onClick(addSingleKey, m => {
|
|
157
|
+
const colDef = this.modelDef.columns[m.data.name]
|
|
158
|
+
log.info(`Add column ${m.data.name}`)
|
|
159
|
+
if (colDef) {
|
|
160
|
+
this.addState({name: colDef.name})
|
|
161
|
+
this.updateColumnEditors()
|
|
162
|
+
this.dirty()
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
alert(`Unknown column name '${m.data.name}'`)
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
154
169
|
this.onClick(addKey, m => {
|
|
155
170
|
const onSelected = (columns: string[]) => {
|
|
156
171
|
log.info(`Adding ${columns.length} columns`, columns)
|
|
@@ -167,6 +182,7 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
167
182
|
}
|
|
168
183
|
|
|
169
184
|
renderContent(parent: PartTag): void {
|
|
185
|
+
// fields
|
|
170
186
|
parent.div('.tt-flex.tt-form.padded.gap.justify-end.align-center', row => {
|
|
171
187
|
row.div('.tt-compound-field', field => {
|
|
172
188
|
field.label().text("Prefix:")
|
|
@@ -174,6 +190,8 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
174
190
|
})
|
|
175
191
|
row.div('.stretch').text("All columns will be prefixed with this")
|
|
176
192
|
})
|
|
193
|
+
|
|
194
|
+
// the table of column editors
|
|
177
195
|
parent.div('.dd-columns-editor-table', table => {
|
|
178
196
|
table.div('.dd-editor-header', header => {
|
|
179
197
|
header.div('.name').label({text: "Name"})
|
|
@@ -184,6 +202,36 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
184
202
|
this.renderCollection(table, 'columns')
|
|
185
203
|
.class('dd-editor-row-container')
|
|
186
204
|
})
|
|
205
|
+
|
|
206
|
+
// common column quick links
|
|
207
|
+
const includedNames = new Set(this.columnStates.map(s => s.name))
|
|
208
|
+
const commonCols = Object.values(this.modelDef.columns).filter(c => c.metadata?.visibility == 'common' && !includedNames.has(c.name))
|
|
209
|
+
if (commonCols.length) {
|
|
210
|
+
parent.h3('.centered.large-top-padding', h3 => {
|
|
211
|
+
h3.span().text("Common Columns")
|
|
212
|
+
})
|
|
213
|
+
parent.table('.dd-table', table => {
|
|
214
|
+
table.thead(thead => {
|
|
215
|
+
thead.tr(tr => {
|
|
216
|
+
tr.th().text("Name")
|
|
217
|
+
tr.th().text("Description")
|
|
218
|
+
tr.th().text("")
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
table.tbody(tbody => {
|
|
222
|
+
for (const colDef of commonCols) {
|
|
223
|
+
tbody.tr(tr => {
|
|
224
|
+
tr.td('.name').text(colDef.name)
|
|
225
|
+
tr.td('.description').text(colDef.metadata?.description || '')
|
|
226
|
+
tr.td().a('.add-column.tt-button.secondary.circle.inline', a => {
|
|
227
|
+
a.i('.glyp-plus')
|
|
228
|
+
}).emitClick(addSingleKey, {name: colDef.name})
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
187
235
|
}
|
|
188
236
|
|
|
189
237
|
removeColumn(id: string) {
|
|
@@ -200,6 +248,7 @@ export class ColumnsEditorModal extends ModalPart<ColumnsEditorState> {
|
|
|
200
248
|
})
|
|
201
249
|
const tableData = await this.tableFields.serialize()
|
|
202
250
|
this.state.tableView.updateColumns(columns, tableData.prefix)
|
|
251
|
+
this.emitMessage(DiveEditor.diveChangedKey, {})
|
|
203
252
|
this.pop()
|
|
204
253
|
}
|
|
205
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-
|
|
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
|
-
|
|
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!:
|
|
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.
|
|
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
|
-
|
|
587
|
-
|
|
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
|
-
|
|
108
|
+
|
|
109
|
+
const newJoins = Object.values(this.modelDef.belongs_to)
|
|
107
110
|
.filter(bt => !existingJoins.has(bt.name))
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
package/terrier/dropdowns.ts
CHANGED
|
@@ -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 (
|
|
82
|
-
|
|
83
|
-
|
|
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/gen/hub-icons.ts
CHANGED
|
@@ -249,6 +249,10 @@ import ReactionRaw from '../images/icons/reaction.svg?raw'
|
|
|
249
249
|
// @ts-ignore
|
|
250
250
|
import ReactionSrc from '../images/icons/reaction.svg'
|
|
251
251
|
// @ts-ignore
|
|
252
|
+
import ReadMailRaw from '../images/icons/read_mail.svg?raw'
|
|
253
|
+
// @ts-ignore
|
|
254
|
+
import ReadMailSrc from '../images/icons/read_mail.svg'
|
|
255
|
+
// @ts-ignore
|
|
252
256
|
import RecentRaw from '../images/icons/recent.svg?raw'
|
|
253
257
|
// @ts-ignore
|
|
254
258
|
import RecentSrc from '../images/icons/recent.svg'
|
|
@@ -578,6 +582,10 @@ export const IconDefs: Record<HubIconName,{ raw: string, src: string }> = {
|
|
|
578
582
|
raw: ReactionRaw,
|
|
579
583
|
src: ReactionSrc,
|
|
580
584
|
},
|
|
585
|
+
"hub-read_mail": {
|
|
586
|
+
raw: ReadMailRaw,
|
|
587
|
+
src: ReadMailSrc,
|
|
588
|
+
},
|
|
581
589
|
"hub-recent": {
|
|
582
590
|
raw: RecentRaw,
|
|
583
591
|
src: RecentSrc,
|
|
@@ -665,7 +673,7 @@ export const IconDefs: Record<HubIconName,{ raw: string, src: string }> = {
|
|
|
665
673
|
}
|
|
666
674
|
|
|
667
675
|
const Names = [
|
|
668
|
-
'hub-active', 'hub-admin', 'hub-archive', 'hub-arrow_down', 'hub-arrow_left', 'hub-arrow_right', 'hub-arrow_up', 'hub-assign', 'hub-attachment', 'hub-back', 'hub-badge', 'hub-board', 'hub-branch', 'hub-bug', 'hub-calculator', 'hub-checkmark', 'hub-close', 'hub-clypboard', 'hub-comment', 'hub-complete', 'hub-dashboard', 'hub-data_pull', 'hub-data_update', 'hub-database', 'hub-day', 'hub-delete', 'hub-documentation', 'hub-edit', 'hub-feature', 'hub-flex', 'hub-forward', 'hub-github', 'hub-history', 'hub-home', 'hub-image', 'hub-inbox', 'hub-info', 'hub-issue', 'hub-lane', 'hub-lane_asap', 'hub-lane_days', 'hub-lane_hours', 'hub-lane_weeks', 'hub-lanes_board', 'hub-level_complete', 'hub-level_highway', 'hub-level_on_ramp', 'hub-level_parking', 'hub-minus', 'hub-night', 'hub-origin', 'hub-pending', 'hub-plus', 'hub-post', 'hub-pr_closed', 'hub-pr_merged', 'hub-pr_open', 'hub-prioritized', 'hub-project', 'hub-question', 'hub-reaction', 'hub-recent', 'hub-refresh', 'hub-request', 'hub-settings', 'hub-status', 'hub-step_deploy', 'hub-step_develop', 'hub-step_investigate', 'hub-step_review', 'hub-step_test', 'hub-steps', 'hub-steps_board', 'hub-subscribe', 'hub-support', 'hub-terrier', 'hub-thumbs_up', 'hub-type', 'hub-unprioritized', 'hub-upload', 'hub-user', 'hub-users'
|
|
676
|
+
'hub-active', 'hub-admin', 'hub-archive', 'hub-arrow_down', 'hub-arrow_left', 'hub-arrow_right', 'hub-arrow_up', 'hub-assign', 'hub-attachment', 'hub-back', 'hub-badge', 'hub-board', 'hub-branch', 'hub-bug', 'hub-calculator', 'hub-checkmark', 'hub-close', 'hub-clypboard', 'hub-comment', 'hub-complete', 'hub-dashboard', 'hub-data_pull', 'hub-data_update', 'hub-database', 'hub-day', 'hub-delete', 'hub-documentation', 'hub-edit', 'hub-feature', 'hub-flex', 'hub-forward', 'hub-github', 'hub-history', 'hub-home', 'hub-image', 'hub-inbox', 'hub-info', 'hub-issue', 'hub-lane', 'hub-lane_asap', 'hub-lane_days', 'hub-lane_hours', 'hub-lane_weeks', 'hub-lanes_board', 'hub-level_complete', 'hub-level_highway', 'hub-level_on_ramp', 'hub-level_parking', 'hub-minus', 'hub-night', 'hub-origin', 'hub-pending', 'hub-plus', 'hub-post', 'hub-pr_closed', 'hub-pr_merged', 'hub-pr_open', 'hub-prioritized', 'hub-project', 'hub-question', 'hub-reaction', 'hub-read_mail', 'hub-recent', 'hub-refresh', 'hub-request', 'hub-settings', 'hub-status', 'hub-step_deploy', 'hub-step_develop', 'hub-step_investigate', 'hub-step_review', 'hub-step_test', 'hub-steps', 'hub-steps_board', 'hub-subscribe', 'hub-support', 'hub-terrier', 'hub-thumbs_up', 'hub-type', 'hub-unprioritized', 'hub-upload', 'hub-user', 'hub-users'
|
|
669
677
|
] as const
|
|
670
678
|
|
|
671
679
|
export type HubIconName = typeof Names[number]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1002.667" height="1002.667" version="1.0" viewBox="0 0 752 752"><path d="M369.5 158.9c-1.6.4-4.6 1.6-6.6 2.7-2 1-47 33.2-99.9 71.4-67.2 48.5-97.2 70.7-99.2 73.4-6.1 8.5-5.9 2.2-5.6 140.2l.3 125.9 2.3 4.6c2.9 5.9 8.2 11.2 14.1 14.1l4.6 2.3h393l4.5-2.1c5.6-2.7 11.3-8.3 14.2-14.3l2.3-4.6.3-125.9c.3-138.1.5-131.7-5.7-140.2-2-2.9-30.9-24.3-99.4-73.6-53-38.3-98.3-70.6-100.6-71.7-4.5-2.3-13.9-3.5-18.6-2.2zm77.1 113.6c37.9 27.3 69 50 69.2 50.4.4 1.1-138.7 101.3-140.2 100.9-1.6-.4-139.6-99.9-139.6-100.7 0-.3 7.3-5.9 16.3-12.3 8.9-6.4 40.3-29 69.7-50.2 29.4-21.2 54-38.4 54.6-38.2.6.2 32.1 22.8 70 50.1zm-158.3 159c38.8 28 72.7 52.1 75.3 53.5 6.4 3.3 15.2 3.8 21.9 1.3 3.1-1.2 32-21.4 75-52.4 38.5-27.7 71.5-51.5 73.3-52.7l3.2-2.3V537H215v-79c0-65.6.2-79 1.3-78.3.8.4 33.1 23.7 72 51.8z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1002.667" height="1002.667" version="1.0" viewBox="0 0 752 752"><path d="M369.5 158.9c-1.6.4-4.6 1.6-6.6 2.7-2 1-47 33.2-99.9 71.4-67.2 48.5-97.2 70.7-99.2 73.4-6.1 8.5-5.9 2.2-5.6 140.2l.3 125.9 2.3 4.6c2.9 5.9 8.2 11.2 14.1 14.1l4.6 2.3h393l4.5-2.1c5.6-2.7 11.3-8.3 14.2-14.3l2.3-4.6.3-125.9c.3-138.1.5-131.7-5.7-140.2-2-2.9-30.9-24.3-99.4-73.6-53-38.3-98.3-70.6-100.6-71.7-4.5-2.3-13.9-3.5-18.6-2.2zm77.1 113.6c37.9 27.3 69 50 69.2 50.4.4 1.1-138.7 101.3-140.2 100.9-1.6-.4-139.6-99.9-139.6-100.7 0-.3 7.3-5.9 16.3-12.3 8.9-6.4 40.3-29 69.7-50.2 29.4-21.2 54-38.4 54.6-38.2.6.2 32.1 22.8 70 50.1zm-158.3 159c38.8 28 72.7 52.1 75.3 53.5 6.4 3.3 15.2 3.8 21.9 1.3 3.1-1.2 32-21.4 75-52.4 38.5-27.7 71.5-51.5 73.3-52.7l3.2-2.3V537H215v-79c0-65.6.2-79 1.3-78.3.8.4 33.1 23.7 72 51.8z"/></svg>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<?xml version="1.0" standalone="no"?>
|
|
2
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
|
3
|
+
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
|
4
|
+
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="752.000000pt" height="752.000000pt" viewBox="0 0 752.000000 752.000000"
|
|
6
|
+
preserveAspectRatio="xMidYMid meet">
|
|
7
|
+
|
|
8
|
+
<g transform="translate(0.000000,752.000000) scale(0.100000,-0.100000)"
|
|
9
|
+
fill="#000000" stroke="none">
|
|
10
|
+
<path d="M3695 5931 c-16 -4 -46 -16 -66 -27 -20 -10 -470 -332 -999 -714
|
|
11
|
+
-672 -485 -972 -707 -992 -734 -61 -85 -59 -22 -56 -1402 l3 -1259 23 -46 c29
|
|
12
|
+
-59 82 -112 141 -141 l46 -23 1965 0 1965 0 45 21 c56 27 113 83 142 143 l23
|
|
13
|
+
46 3 1259 c3 1381 5 1317 -57 1402 -20 29 -309 243 -994 736 -530 383 -983
|
|
14
|
+
706 -1006 717 -45 23 -139 35 -186 22z m771 -1136 c379 -273 690 -500 692
|
|
15
|
+
-504 4 -11 -1387 -1013 -1402 -1009 -16 4 -1396 999 -1396 1007 0 3 73 59 163
|
|
16
|
+
123 89 64 403 290 697 502 294 212 540 384 546 382 6 -2 321 -228 700 -501z
|
|
17
|
+
m-1583 -1590 c388 -280 727 -521 753 -535 64 -33 152 -38 219 -13 31 12 320
|
|
18
|
+
214 750 524 385 277 715 515 733 527 l32 23 0 -790 0 -791 -1610 0 -1610 0 0
|
|
19
|
+
790 c0 656 2 790 13 783 8 -4 331 -237 720 -518z"/>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
package/terrier/overlays.ts
CHANGED
|
@@ -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
|
-
*
|
|
278
|
-
* @param elem
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
163
|
-
|
|
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' | '
|
|
11
|
+
export type MetaVisibility = 'common' | 'uncommon' | 'hidden'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Definition for a single column in the schema.
|
|
@@ -20,7 +20,10 @@ export type ColumnDef = {
|
|
|
20
20
|
type: string
|
|
21
21
|
possible_values?: string[]
|
|
22
22
|
default?: string
|
|
23
|
-
|
|
23
|
+
metadata?: {
|
|
24
|
+
description?: string
|
|
25
|
+
visibility?: MetaVisibility
|
|
26
|
+
}
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
/**
|
|
@@ -84,12 +87,13 @@ async function get(): Promise<SchemaDef> {
|
|
|
84
87
|
* @param belongsTo
|
|
85
88
|
*/
|
|
86
89
|
function belongsToDisplay(belongsTo: BelongsToDef): string {
|
|
90
|
+
const btName = inflection.titleize(belongsTo.name)
|
|
87
91
|
if (belongsTo.name != inflection.singularize(inflection.tableize(belongsTo.model))) {
|
|
88
92
|
// the model is different than the name of the association
|
|
89
|
-
return `${
|
|
93
|
+
return `${btName} (${belongsTo.model})`
|
|
90
94
|
}
|
|
91
95
|
else {
|
|
92
|
-
return
|
|
96
|
+
return btName
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
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
|
}
|