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.
- package/data-dive/dives/dive-editor.ts +25 -2
- package/data-dive/queries/columns.ts +2 -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/overlays.ts +42 -6
- package/terrier/parts/content-part.ts +4 -5
- package/terrier/schema.ts +4 -3
- 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
|
|
|
@@ -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-
|
|
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/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.
|
|
@@ -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 `${
|
|
93
|
+
return `${btName} (${belongsTo.model})`
|
|
93
94
|
}
|
|
94
95
|
else {
|
|
95
|
-
return
|
|
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
|
}
|