terrier-engine 4.24.3 → 4.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data-dive/dives/dive-delivery.ts +60 -0
- package/data-dive/dives/dive-runs.ts +39 -3
- package/package.json +1 -1
- package/terrier/format.ts +8 -1
- package/terrier/hints.ts +182 -0
- package/terrier/parts/page-part.ts +13 -5
- package/terrier/theme.ts +9 -0
|
@@ -4,9 +4,19 @@ import TerrierPart from "../../terrier/parts/terrier-part"
|
|
|
4
4
|
import {RegularSchedule, RegularScheduleForm} from "../../terrier/schedules"
|
|
5
5
|
import {Logger} from "tuff-core/logging"
|
|
6
6
|
import {EmailListForm} from "../../terrier/emails"
|
|
7
|
+
import {DdDiveRun} from "../gen/models"
|
|
8
|
+
import Db from "../dd-db"
|
|
9
|
+
import dayjs from "dayjs"
|
|
10
|
+
import Dates from "../queries/dates"
|
|
11
|
+
import DiveRuns from "./dive-runs";
|
|
7
12
|
|
|
8
13
|
const log = new Logger("Dive Delivery")
|
|
9
14
|
|
|
15
|
+
|
|
16
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
17
|
+
// Delivery Form
|
|
18
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
19
|
+
|
|
10
20
|
export type DiveDeliverySettings = {
|
|
11
21
|
delivery_schedule: RegularSchedule
|
|
12
22
|
delivery_recipients: string[]
|
|
@@ -16,11 +26,13 @@ export class DiveDeliveryForm extends TerrierPart<DiveEditorState> {
|
|
|
16
26
|
|
|
17
27
|
scheduleForm!: RegularScheduleForm
|
|
18
28
|
recipientsForm!: EmailListForm
|
|
29
|
+
deliveryList!: DiveDeliveryList
|
|
19
30
|
|
|
20
31
|
async init() {
|
|
21
32
|
const schedule = this.state.dive.delivery_schedule || {schedule_type: 'none'}
|
|
22
33
|
this.scheduleForm = this.makePart(RegularScheduleForm, schedule)
|
|
23
34
|
this.recipientsForm = this.makePart(EmailListForm, {emails: this.state.dive.delivery_recipients || []})
|
|
35
|
+
this.deliveryList = this.makePart(DiveDeliveryList, this.state)
|
|
24
36
|
|
|
25
37
|
this.listen('datachanged', this.scheduleForm.dataChangedKey, m => {
|
|
26
38
|
log.info(`Schedule form data changed`, m.data)
|
|
@@ -42,6 +54,11 @@ export class DiveDeliveryForm extends TerrierPart<DiveEditorState> {
|
|
|
42
54
|
parent.part(this.scheduleForm)
|
|
43
55
|
parent.h3(".glyp-users").text("Recipients")
|
|
44
56
|
parent.part(this.recipientsForm)
|
|
57
|
+
parent.div('.deliveries', deliveriesContainer => {
|
|
58
|
+
// it looks better without the gap between the header and list
|
|
59
|
+
deliveriesContainer.h3(".glyp-inbox").text("Deliveries")
|
|
60
|
+
deliveriesContainer.part(this.deliveryList)
|
|
61
|
+
})
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
/**
|
|
@@ -55,3 +72,46 @@ export class DiveDeliveryForm extends TerrierPart<DiveEditorState> {
|
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
78
|
+
// Delivery List
|
|
79
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
80
|
+
|
|
81
|
+
class DiveDeliveryList extends TerrierPart<DiveEditorState> {
|
|
82
|
+
|
|
83
|
+
runs: DdDiveRun[] = []
|
|
84
|
+
|
|
85
|
+
async init() {
|
|
86
|
+
this.runs = await Db().query("dd_dive_run")
|
|
87
|
+
.where({dd_dive_id: this.state.dive.id})
|
|
88
|
+
.where("delivery_recipients is not null")
|
|
89
|
+
.orderBy("created_at desc")
|
|
90
|
+
.limit(100)
|
|
91
|
+
.exec()
|
|
92
|
+
|
|
93
|
+
this.dirty()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get parentClasses(): Array<string> {
|
|
97
|
+
return ['dive-delivery-list']
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
render(parent: PartTag) {
|
|
101
|
+
for (const run of this.runs) {
|
|
102
|
+
parent.div(".run", view => {
|
|
103
|
+
view.div(".recipients.glyp-users.with-icon").text(run.delivery_recipients?.join("; ") || "No Recipients")
|
|
104
|
+
view.div('.datetime', dateTimeView => {
|
|
105
|
+
const d = dayjs(run.created_at)
|
|
106
|
+
dateTimeView.div(".date").text(d.format(Dates.displayFormat))
|
|
107
|
+
dateTimeView.div(".time").text(d.format("H:mm A"))
|
|
108
|
+
})
|
|
109
|
+
run.dd_dive = this.state.dive // so that the filename is better
|
|
110
|
+
view.a(".download.glyp-download", {href: DiveRuns.outputUrl(run), target: '_blank'})
|
|
111
|
+
.data({tooltip: "Download the dive results for this delivery"})
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
|
|
@@ -15,12 +15,18 @@ import {TerrierFormFields} from "../../terrier/forms"
|
|
|
15
15
|
import * as inflection from "inflection"
|
|
16
16
|
import Dates, {DateLiteral, DatePeriodPickerPart, DatePeriodPickerState, LiteralDateRange} from "../queries/dates"
|
|
17
17
|
import dayjs from "dayjs"
|
|
18
|
-
import {ProgressBarPart} from "../../terrier/progress"
|
|
19
|
-
import {LogListPart} from "../../terrier/logging"
|
|
18
|
+
import {ProgressBarPart} from "../../terrier/progress"
|
|
19
|
+
import {LogListPart} from "../../terrier/logging"
|
|
20
20
|
import Messages from "tuff-core/messages"
|
|
21
|
+
import Format from "../../terrier/format"
|
|
21
22
|
|
|
22
23
|
const log = new Logger("DiveRuns")
|
|
23
24
|
|
|
25
|
+
|
|
26
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
27
|
+
// Run Modal
|
|
28
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
29
|
+
|
|
24
30
|
type RunQueryResult = {
|
|
25
31
|
id: string
|
|
26
32
|
time: string
|
|
@@ -309,4 +315,34 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
|
|
|
309
315
|
})
|
|
310
316
|
}
|
|
311
317
|
|
|
312
|
-
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
322
|
+
// Utilities
|
|
323
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Generates a relative URL to download the output of the given run with an optional custom file name.
|
|
327
|
+
* @param run
|
|
328
|
+
* @param filename
|
|
329
|
+
*/
|
|
330
|
+
function outputUrl(run: DdDiveRun, filename?: string): string {
|
|
331
|
+
if (!filename?.length) {
|
|
332
|
+
const nameComps: string[] = []
|
|
333
|
+
const dive = run.dd_dive
|
|
334
|
+
if (dive) {
|
|
335
|
+
nameComps.push(inflection.underscore(dive.name))
|
|
336
|
+
}
|
|
337
|
+
const d = dayjs(run.created_at)
|
|
338
|
+
nameComps.push(d.format('YYYY-MM-DD_HHmmss'))
|
|
339
|
+
filename = nameComps.join('_') + '.xlsx'
|
|
340
|
+
}
|
|
341
|
+
return `/data_dive/download_run/${run.id}/${filename}?t=${dayjs().format(Format.timestamp)}`
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const DiveRuns = {
|
|
345
|
+
outputUrl
|
|
346
|
+
}
|
|
347
|
+
export default DiveRuns
|
|
348
|
+
|
package/package.json
CHANGED
package/terrier/format.ts
CHANGED
|
@@ -12,13 +12,20 @@ function cents(c: number | string): string {
|
|
|
12
12
|
return '$' + (num/100).toFixed(2)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
16
|
+
// Dates and Times
|
|
17
|
+
////////////////////////////////////////////////////////////////////////////////
|
|
18
|
+
|
|
19
|
+
const timestamp = 'YYYYMMDDHHMMSS'
|
|
20
|
+
|
|
15
21
|
|
|
16
22
|
////////////////////////////////////////////////////////////////////////////////
|
|
17
23
|
// Export
|
|
18
24
|
////////////////////////////////////////////////////////////////////////////////
|
|
19
25
|
|
|
20
26
|
const Format = {
|
|
21
|
-
cents
|
|
27
|
+
cents,
|
|
28
|
+
timestamp
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
export default Format
|
package/terrier/hints.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import Html, {DivTag, HtmlParentTag} from "tuff-core/html"
|
|
2
|
+
import Messages from "tuff-core/messages"
|
|
3
|
+
import {PartPlugin} from "tuff-core/plugins"
|
|
4
|
+
import Theme, {IconName} from "./theme"
|
|
5
|
+
import PagePart from "./parts/page-part"
|
|
6
|
+
import TerrierPart from "./parts/terrier-part"
|
|
7
|
+
|
|
8
|
+
export type Hint = {
|
|
9
|
+
title?: string
|
|
10
|
+
icon?: IconName
|
|
11
|
+
tooltip?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type HintRenderOptions = {
|
|
15
|
+
classes?: string[]
|
|
16
|
+
side?: 'inline' | 'top' | 'right' | 'bottom' | 'left' | 'inline-top' | 'inline-right' | 'inline-bottom' | 'inline-left'
|
|
17
|
+
hideIcon?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Render a hint into the given tag
|
|
22
|
+
* @param theme the theme to render the hint icon with
|
|
23
|
+
* @param parent the parent tag to render the hint into
|
|
24
|
+
* @param hint the hint to render
|
|
25
|
+
* @param options options to modify how the hint is rendered
|
|
26
|
+
*/
|
|
27
|
+
function renderHint(theme: Theme, parent: HtmlParentTag, hint: Hint, options?: HintRenderOptions): DivTag {
|
|
28
|
+
const hintTag = parent.div('.tt-hint')
|
|
29
|
+
|
|
30
|
+
if (hint.tooltip) hintTag.dataAttr('tooltip', hint.tooltip)
|
|
31
|
+
if (options?.classes) hintTag.class(...options.classes)
|
|
32
|
+
if (options?.side && options.side != 'inline') hintTag.dataAttr('tt-hint-side', options.side)
|
|
33
|
+
|
|
34
|
+
const hideIcon = options?.hideIcon ?? false
|
|
35
|
+
if (!hideIcon) {
|
|
36
|
+
const icon = hint.icon ?? 'glyp-hint'
|
|
37
|
+
theme.renderIcon(hintTag, icon)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (hint.title) hintTag.span('.tt-hint-title').text(hint.title)
|
|
41
|
+
|
|
42
|
+
return hintTag
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Add a hint element to the given parent element
|
|
47
|
+
* @param theme the theme to render the hint icon with
|
|
48
|
+
* @param parentElement the element to add the hint element to
|
|
49
|
+
* @param hint the hint to add
|
|
50
|
+
* @param options options to modify how the hint is rendered
|
|
51
|
+
* @param insertPosition specify where to insert the hint relative to the parent element
|
|
52
|
+
*/
|
|
53
|
+
function injectHint(theme: Theme, parentElement: Element, hint: Hint, options?: HintRenderOptions, insertPosition: InsertPosition = 'beforeend'): HTMLDivElement {
|
|
54
|
+
const elem = Html.createElement('div', div => renderHint(theme, div, hint, options)).firstElementChild as HTMLDivElement
|
|
55
|
+
parentElement.insertAdjacentElement(insertPosition, elem)
|
|
56
|
+
return elem
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type DynamicHint = {
|
|
60
|
+
selector: string
|
|
61
|
+
hint: Hint
|
|
62
|
+
options?: HintRenderOptions
|
|
63
|
+
insertPosition?: InsertPosition
|
|
64
|
+
onlyFirstMatch?: boolean // only add a hint to the first element that matches the selector
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Adds dynamic hints to a given part. Dynamic hints will detect changes to the DOM to update where they are rendered
|
|
69
|
+
* @param part the part to add dynamic hints to. Dynamic hints will only be added to matching elements under the part's root element
|
|
70
|
+
* @param hints the hints to add.
|
|
71
|
+
*/
|
|
72
|
+
function addDynamicHints(part: TerrierPart<any>, hints: DynamicHint[]) {
|
|
73
|
+
part.makePlugin(DynamicHintsPlugin, hints)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class DynamicHintsPlugin extends PartPlugin<DynamicHint[]> {
|
|
77
|
+
observer: MutationObserver = new MutationObserver(this.handleMutations.bind(this))
|
|
78
|
+
|
|
79
|
+
async init() {
|
|
80
|
+
if (!('theme' in this.part)) throw new Error("DynamicHintsPlugin requires a TerrierPart")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
update(elem: HTMLElement) {
|
|
84
|
+
super.update(elem)
|
|
85
|
+
|
|
86
|
+
this.addDynamicHints(elem)
|
|
87
|
+
this.observer.disconnect()
|
|
88
|
+
this.observer.observe(elem, { childList: true, subtree: true })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private handleMutations(mutations: MutationRecord[], _observer: MutationObserver): void {
|
|
92
|
+
// Ignore mutations that added hint elements (prevents infinite recursion)
|
|
93
|
+
const addedHints = mutations.some(m =>
|
|
94
|
+
Array.from(m.addedNodes).some(n => n instanceof HTMLElement && n.dataset.hintSource === 'DynamicHintsPlugin')
|
|
95
|
+
)
|
|
96
|
+
if (addedHints) return
|
|
97
|
+
if (!this.part.element) return
|
|
98
|
+
this.addDynamicHints(this.part.element)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private addDynamicHints(elem: HTMLElement) {
|
|
102
|
+
const theme = (this.part as TerrierPart<any>).theme
|
|
103
|
+
|
|
104
|
+
elem.querySelectorAll('.tt-hint[data-hint-source=DynamicHintsPlugin]').forEach(e => e.remove())
|
|
105
|
+
|
|
106
|
+
for (const dynamicHint of this.state) {
|
|
107
|
+
const matches = elem.querySelectorAll(dynamicHint.selector)
|
|
108
|
+
if (!matches.length) continue
|
|
109
|
+
if (dynamicHint.onlyFirstMatch) {
|
|
110
|
+
this.addDynamicHint(theme, matches[0], dynamicHint)
|
|
111
|
+
} else {
|
|
112
|
+
matches.forEach(match => this.addDynamicHint(theme, match, dynamicHint))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private addDynamicHint(theme: Theme, elem: Element, hint: DynamicHint) {
|
|
118
|
+
const hintElement = injectHint(theme, elem, hint.hint, hint.options, hint.insertPosition ?? 'beforeend')
|
|
119
|
+
hintElement.dataset.hintSource = 'DynamicHintsPlugin'
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Adds a hints toggle checkbox to a page's toolbar and handles persisting the state of the checkbox between visits.
|
|
125
|
+
* @param part the page to add a checkbox to
|
|
126
|
+
* @param hintKey a key for this page so that the checkbox state can be persisted
|
|
127
|
+
*/
|
|
128
|
+
function addHintToggle(part: PagePart<any>, hintKey: string) {
|
|
129
|
+
part.makePlugin(HintTogglePlugin, { hintKey })
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class HintTogglePlugin extends PartPlugin<{ hintKey: string }> {
|
|
133
|
+
|
|
134
|
+
checkboxChangedKey = Messages.untypedKey()
|
|
135
|
+
storageKey = `show-hints-${this.state.hintKey}`
|
|
136
|
+
|
|
137
|
+
get storedValue() {
|
|
138
|
+
const stored = localStorage.getItem(this.storageKey)
|
|
139
|
+
return stored == null ? true : stored == 'true'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
set storedValue(value: boolean) {
|
|
143
|
+
localStorage.setItem(this.storageKey, value.toString())
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async init() {
|
|
147
|
+
document.body.classList.toggle('tt-hints-hidden', !this.storedValue)
|
|
148
|
+
|
|
149
|
+
const part = this.part as PagePart<any>
|
|
150
|
+
part.addToolbarInput('show-hints', 'checkbox', {
|
|
151
|
+
title: "Hints",
|
|
152
|
+
icon: 'glyp-hint',
|
|
153
|
+
onChangeKey: this.checkboxChangedKey,
|
|
154
|
+
onInputKey: this.checkboxChangedKey,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
part.onInput(this.checkboxChangedKey, m => {
|
|
158
|
+
const checked = (m.event.target as HTMLInputElement).checked
|
|
159
|
+
this.storedValue = checked
|
|
160
|
+
document.body.classList.toggle('tt-hints-hidden', !checked)
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
update(elem: HTMLElement) {
|
|
165
|
+
super.update(elem)
|
|
166
|
+
|
|
167
|
+
const checkbox = elem.querySelector(`[data-toolbar-field-name=show-hints]`)
|
|
168
|
+
|
|
169
|
+
if (checkbox instanceof HTMLInputElement) {
|
|
170
|
+
checkbox.checked = this.storedValue
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const Hints = {
|
|
176
|
+
renderHint,
|
|
177
|
+
injectHint,
|
|
178
|
+
addDynamicHints,
|
|
179
|
+
addHintToggle,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default Hints
|
|
@@ -4,6 +4,7 @@ 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 {InputTagAttrs, SelectTagAttrs} from "tuff-core/html"
|
|
7
8
|
|
|
8
9
|
const log = new Logger("Terrier PagePart")
|
|
9
10
|
|
|
@@ -35,10 +36,10 @@ type ToolbarFieldDefOptions = {
|
|
|
35
36
|
icon?: IconName
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
type ToolbarSelectDef = { type: 'select', options: SelectOptions } & BaseFieldDef
|
|
39
|
+
type ToolbarSelectDef = { type: 'select', options: SelectOptions, attrs?: SelectTagAttrs } & BaseFieldDef
|
|
39
40
|
|
|
40
41
|
type ValuedInputType = 'text' | 'color' | 'date' | 'datetime-local' | 'email' | 'hidden' | 'month' | 'number' | 'password' | 'search' | 'tel' | 'time' | 'url' | 'week' | 'checkbox'
|
|
41
|
-
type ToolbarValuedInputDef = { type: ValuedInputType } & BaseFieldDef
|
|
42
|
+
type ToolbarValuedInputDef = { type: ValuedInputType, attrs?: InputTagAttrs } & BaseFieldDef
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* Defines a field to be rendered in the page's toolbar
|
|
@@ -90,7 +91,7 @@ export default abstract class PagePart<TState> extends ContentPart<TState> {
|
|
|
90
91
|
* @param selectOptions an array of select options
|
|
91
92
|
* @param opts
|
|
92
93
|
*/
|
|
93
|
-
addToolbarSelect(name: string, selectOptions: SelectOptions, opts?: ToolbarFieldDefOptions) {
|
|
94
|
+
addToolbarSelect(name: string, selectOptions: SelectOptions, opts?: ToolbarFieldDefOptions & { attrs?: SelectTagAttrs }) {
|
|
94
95
|
this.addToolbarFieldDef({ type: 'select', name, options: selectOptions, ...opts })
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -100,7 +101,7 @@ export default abstract class PagePart<TState> extends ContentPart<TState> {
|
|
|
100
101
|
* @param type the type attribute of the input field
|
|
101
102
|
* @param opts
|
|
102
103
|
*/
|
|
103
|
-
addToolbarInput(name: string, type: ValuedInputType, opts?: ToolbarFieldDefOptions) {
|
|
104
|
+
addToolbarInput(name: string, type: ValuedInputType, opts?: ToolbarFieldDefOptions & { attrs?: InputTagAttrs }) {
|
|
104
105
|
this.addToolbarFieldDef({ type, name, ...opts })
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -203,6 +204,10 @@ export default abstract class PagePart<TState> extends ContentPart<TState> {
|
|
|
203
204
|
})
|
|
204
205
|
if (def.onChangeKey) select.emitChange(def.onChangeKey)
|
|
205
206
|
if (def.onInputKey) select.emitInput(def.onInputKey)
|
|
207
|
+
|
|
208
|
+
if (def.attrs) select.attrs(def.attrs)
|
|
209
|
+
|
|
210
|
+
select.dataAttr('toolbar-field-name', name)
|
|
206
211
|
} else {
|
|
207
212
|
const input = label.input({name: def.name, type: def.type, value: def.defaultValue})
|
|
208
213
|
if (def.type == 'checkbox' && def.defaultValue?.length) {
|
|
@@ -210,8 +215,11 @@ export default abstract class PagePart<TState> extends ContentPart<TState> {
|
|
|
210
215
|
}
|
|
211
216
|
if (def.onChangeKey) input.emitChange(def.onChangeKey)
|
|
212
217
|
if (def.onInputKey) input.emitInput(def.onInputKey)
|
|
213
|
-
}
|
|
214
218
|
|
|
219
|
+
if (def.attrs) input.attrs(def.attrs)
|
|
220
|
+
|
|
221
|
+
input.dataAttr('toolbar-field-name', name)
|
|
222
|
+
}
|
|
215
223
|
|
|
216
224
|
if (def.tooltip?.length) label.dataAttr('tooltip', def.tooltip)
|
|
217
225
|
})
|
package/terrier/theme.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {PartTag} from "tuff-core/parts"
|
|
|
2
2
|
import {GlypName} from "./glyps"
|
|
3
3
|
import HubIcons, {HubIconName} from "./gen/hub-icons"
|
|
4
4
|
import {Key} from "tuff-core/messages"
|
|
5
|
+
import Hints, {Hint, HintRenderOptions} from "./hints";
|
|
5
6
|
|
|
6
7
|
export interface ThemeType {
|
|
7
8
|
readonly icons: string
|
|
@@ -38,6 +39,10 @@ export type Action = {
|
|
|
38
39
|
classes?: string[]
|
|
39
40
|
click?: Packet
|
|
40
41
|
badge?: string
|
|
42
|
+
hint?: {
|
|
43
|
+
hint: Hint
|
|
44
|
+
options?: HintRenderOptions
|
|
45
|
+
}
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
/**
|
|
@@ -115,6 +120,10 @@ export default class Theme {
|
|
|
115
120
|
const badgeColor = options?.badgeColor || 'alert'
|
|
116
121
|
a.div(`.badge.${badgeColor}`, {text: action.badge})
|
|
117
122
|
}
|
|
123
|
+
|
|
124
|
+
if (action.hint) {
|
|
125
|
+
Hints.renderHint(this, a, action.hint.hint, action.hint.options)
|
|
126
|
+
}
|
|
118
127
|
})
|
|
119
128
|
}
|
|
120
129
|
}
|