terrier-engine 4.56.2 → 4.57.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 +9 -3
- package/data-dive/dives/dive-editor.ts +10 -2
- package/data-dive/dives/dive-runs.ts +4 -1
- package/data-dive/dives/dive-settings.ts +4 -0
- package/package.json +2 -2
- package/terrier/definitions/tiny-modal.d.ts +65 -0
- package/terrier/schedules.ts +143 -84
- package/terrier/util-types.ts +4 -0
- package/terrier/utils.ts +38 -0
|
@@ -228,8 +228,11 @@ class DiveDistributionModal extends ModalPart<DiveDistributionEditorState> {
|
|
|
228
228
|
|
|
229
229
|
this.setIcon('glyp-email')
|
|
230
230
|
|
|
231
|
-
this.scheduleFields = new RegularScheduleFields(this, this.dist.schedule
|
|
232
|
-
|
|
231
|
+
this.scheduleFields = new RegularScheduleFields(this, this.dist.schedule, {
|
|
232
|
+
showNoneOption: false,
|
|
233
|
+
optionTitle: (type, title) =>
|
|
234
|
+
`${type == 'monthanchored' ? "Deliver on" : "Deliver"} ${title}`
|
|
235
|
+
})
|
|
233
236
|
this.recipientsForm = this.makePart(EmailListForm, { emails: this.dist.recipients || [] })
|
|
234
237
|
|
|
235
238
|
// save
|
|
@@ -267,9 +270,12 @@ class DiveDistributionModal extends ModalPart<DiveDistributionEditorState> {
|
|
|
267
270
|
})
|
|
268
271
|
}
|
|
269
272
|
|
|
273
|
+
get contentClasses() {
|
|
274
|
+
return ['padded', ...super.contentClasses]
|
|
275
|
+
}
|
|
270
276
|
|
|
271
277
|
renderContent(parent: PartTag): void {
|
|
272
|
-
parent.div(".tt-
|
|
278
|
+
parent.div(".tt-grid.large-gap", row => {
|
|
273
279
|
row.div('.stretch.tt-flex.column.gap', col => {
|
|
274
280
|
col.h3(".glyp-setup.text-center").text("Schedule")
|
|
275
281
|
this.scheduleFields.render(col)
|
|
@@ -351,8 +351,12 @@ class NewQueryModal extends ModalPart<NewQueryState> {
|
|
|
351
351
|
})
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
+
get contentClasses() {
|
|
355
|
+
return ['padded', ...super.contentClasses]
|
|
356
|
+
}
|
|
357
|
+
|
|
354
358
|
renderContent(parent: PartTag): void {
|
|
355
|
-
parent.div('.tt-flex.tt-form.
|
|
359
|
+
parent.div('.tt-flex.tt-form.column.gap.dd-new-query-form', col => {
|
|
356
360
|
col.part(this.settingsForm)
|
|
357
361
|
col.part(this.modelPicker)
|
|
358
362
|
})
|
|
@@ -423,8 +427,12 @@ class DuplicateQueryModal extends ModalPart<DuplicateQueryState> {
|
|
|
423
427
|
this.emitMessage(DiveEditor.diveChangedKey, {})
|
|
424
428
|
}
|
|
425
429
|
|
|
430
|
+
get contentClasses() {
|
|
431
|
+
return ['padded', ...super.contentClasses]
|
|
432
|
+
}
|
|
433
|
+
|
|
426
434
|
renderContent(parent: PartTag): void {
|
|
427
|
-
parent.div(".tt-flex.column.
|
|
435
|
+
parent.div(".tt-flex.column.gap", col => {
|
|
428
436
|
Fragments.simpleHeading(col, this.theme, "Name")
|
|
429
437
|
this.fields.textInput(col, 'name')
|
|
430
438
|
})
|
|
@@ -183,9 +183,12 @@ export class DiveRunModal extends ModalPart<{dive: DdDive }> {
|
|
|
183
183
|
})
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
get contentClasses() {
|
|
187
|
+
return ['padded', ...super.contentClasses]
|
|
188
|
+
}
|
|
186
189
|
|
|
187
190
|
renderContent(parent: PartTag): void {
|
|
188
|
-
parent.div('.tt-flex.
|
|
191
|
+
parent.div('.tt-flex.gap.column', col => {
|
|
189
192
|
col.part(this.progressBar)
|
|
190
193
|
|
|
191
194
|
// inputs and outputs row
|
|
@@ -182,6 +182,10 @@ export class DiveSettingsModal extends ModalPart<DiveSettingsState> {
|
|
|
182
182
|
})
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
get contentClasses() {
|
|
186
|
+
return ['padded', ...super.contentClasses]
|
|
187
|
+
}
|
|
188
|
+
|
|
185
189
|
renderContent(parent: PartTag): void {
|
|
186
190
|
parent.div('.tt-flex.tt-form.padded.column.gap.dd-new-dive-form', col => {
|
|
187
191
|
col.part(this.settingsForm)
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"files": [
|
|
5
5
|
"*"
|
|
6
6
|
],
|
|
7
|
-
"version": "4.
|
|
7
|
+
"version": "4.57.0",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "https://github.com/Terrier-Tech/terrier-engine"
|
|
@@ -33,6 +33,6 @@
|
|
|
33
33
|
"typescript": "^5.6.3",
|
|
34
34
|
"vite": "^5.4.19",
|
|
35
35
|
"vite-plugin-ruby": "^5.1.1",
|
|
36
|
-
"vitest": "^
|
|
36
|
+
"vitest": "^2.1.9"
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Simplify } from "../util-types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base options for all tiny-modal alerts
|
|
5
|
+
*/
|
|
6
|
+
export type TinyModalAlertOptions = {
|
|
7
|
+
title?: string
|
|
8
|
+
body?: string
|
|
9
|
+
icon?: string | string[]
|
|
10
|
+
classes?: string | string[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for tinyModal.showAlert
|
|
15
|
+
*/
|
|
16
|
+
export type TinyModalShowAlertOptions = Simplify<TinyModalAlertOptions & { actions?: TinyModalAlertActionWithCallback[] }>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Options for tinyModal.async.showAlert
|
|
20
|
+
*/
|
|
21
|
+
export type TinyModalAsyncShowAlertOptions = Simplify<TinyModalAlertOptions & { actions?: TinyModalAlertAction[] }>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Options for tinyModal.confirmAlert and tinyModal.async.confirmAlert
|
|
25
|
+
*/
|
|
26
|
+
export type TinyModalConfirmAlertOptions = Simplify<TinyModalAlertOptions & {
|
|
27
|
+
confirmTitle?: string
|
|
28
|
+
confirmIcon?: string | string[]
|
|
29
|
+
confirmClasses?: string | string[]
|
|
30
|
+
cancelTitle?: string
|
|
31
|
+
cancelIcon?: string | string[]
|
|
32
|
+
cancelClasses?: string | string[]
|
|
33
|
+
}>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Base type for an action in a tiny-modal alert
|
|
37
|
+
*/
|
|
38
|
+
export type TinyModalAlertAction = {
|
|
39
|
+
name?: string
|
|
40
|
+
title?: string
|
|
41
|
+
icon?: string | string[]
|
|
42
|
+
classes?: string | string[]
|
|
43
|
+
href?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Type for an action in a non-async tiny-modal alert
|
|
48
|
+
*/
|
|
49
|
+
export type TinyModalAlertActionWithCallback = Simplify<TinyModalAlertAction & { callback?: () => void }>
|
|
50
|
+
|
|
51
|
+
export type TinyModalGlobals = {
|
|
52
|
+
close: () => void
|
|
53
|
+
|
|
54
|
+
closeAlert: () => void
|
|
55
|
+
showAlert: (options: TinyModalShowAlertOptions) => void
|
|
56
|
+
confirmAlert: (title: string, body: string, callback: () => void, options?: TinyModalConfirmAlertOptions) => void
|
|
57
|
+
noticeAlert: (title: string, body: string, action?: TinyModalAlertActionWithCallback, options?: TinyModalAlertOptions) => void
|
|
58
|
+
alertAlert: (title: string, body: string, action?: TinyModalAlertActionWithCallback, options?: TinyModalAlertOptions) => void
|
|
59
|
+
async: {
|
|
60
|
+
showAlert: (options: TinyModalAsyncShowAlertOptions) => Promise<TinyModalAlertAction>
|
|
61
|
+
confirmAlert: (title: string, body: string, options?: TinyModalConfirmAlertOptions) => Promise<boolean>
|
|
62
|
+
noticeAlert: (title: string, body: string, action?: TinyModalAlertAction, options?: TinyModalAlertOptions) => Promise<void>
|
|
63
|
+
alertAlert: (title: string, body: string, action?: TinyModalAlertAction, options?: TinyModalAlertOptions) => Promise<void>
|
|
64
|
+
}
|
|
65
|
+
}
|
package/terrier/schedules.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import dayjs from "dayjs"
|
|
2
2
|
import * as inflection from "inflection"
|
|
3
|
+
import Forms from "tuff-core/forms"
|
|
4
|
+
import { Logger } from "tuff-core/logging"
|
|
3
5
|
import Messages from "tuff-core/messages"
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {TerrierFormFields} from "./forms"
|
|
6
|
+
import { PartTag } from "tuff-core/parts"
|
|
7
|
+
import { TerrierFormFields } from "./forms"
|
|
7
8
|
import TerrierPart from "./parts/terrier-part"
|
|
9
|
+
import { unreachable } from "./utils"
|
|
8
10
|
|
|
9
11
|
const log = new Logger("Schedules")
|
|
10
12
|
|
|
@@ -23,7 +25,7 @@ const HoursOfDay = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'
|
|
|
23
25
|
export type HourOfDay = typeof HoursOfDay[number]
|
|
24
26
|
|
|
25
27
|
const HourOfDayOptions = HoursOfDay.map(h => {
|
|
26
|
-
return {value: h.toString(), title: dayjs().hour(parseInt(h)).format('h A')}
|
|
28
|
+
return { value: h.toString(), title: dayjs().hour(parseInt(h)).format('h A') }
|
|
27
29
|
})
|
|
28
30
|
|
|
29
31
|
type BaseSchedule = {
|
|
@@ -35,13 +37,17 @@ const DaysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'fri
|
|
|
35
37
|
export type DayOfWeek = typeof DaysOfWeek[number]
|
|
36
38
|
|
|
37
39
|
const DayOfWeekOptions = DaysOfWeek.map(d => {
|
|
38
|
-
return {value: d.toString(), title: inflection.capitalize(d)}
|
|
40
|
+
return { value: d.toString(), title: inflection.capitalize(d) }
|
|
39
41
|
})
|
|
40
42
|
|
|
41
43
|
export type DailySchedule = BaseSchedule & {
|
|
42
44
|
schedule_type: 'daily'
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
export type WeekdailySchedule = BaseSchedule & {
|
|
48
|
+
schedule_type: 'weekdaily'
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
export type WeeklySchedule = BaseSchedule & {
|
|
46
52
|
schedule_type: 'weekly'
|
|
47
53
|
day_of_week: DayOfWeek
|
|
@@ -52,7 +58,7 @@ const DaysOfMonth = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '1
|
|
|
52
58
|
export type DayOfMonth = typeof DaysOfMonth[number]
|
|
53
59
|
|
|
54
60
|
const DayOfMonthOptions = DaysOfMonth.map(d => {
|
|
55
|
-
return {value: d, title: inflection.ordinalize(d)}
|
|
61
|
+
return { value: d, title: inflection.ordinalize(d) }
|
|
56
62
|
})
|
|
57
63
|
|
|
58
64
|
export type MonthlySchedule = BaseSchedule & {
|
|
@@ -60,98 +66,137 @@ export type MonthlySchedule = BaseSchedule & {
|
|
|
60
66
|
day_of_month: DayOfMonth
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
export const MonthAnchors = {
|
|
70
|
+
first_day: "First day",
|
|
71
|
+
first_weekday: "First weekday",
|
|
72
|
+
last_day: "Last day",
|
|
73
|
+
last_weekday: "Last weekday",
|
|
74
|
+
} as const
|
|
75
|
+
export type MonthAnchor = keyof typeof MonthAnchors
|
|
76
|
+
|
|
77
|
+
const MonthAnchorOptions = Forms.objectToSelectOptions(MonthAnchors)
|
|
78
|
+
|
|
79
|
+
export type MonthAnchoredSchedule = BaseSchedule & {
|
|
80
|
+
schedule_type: 'monthanchored'
|
|
81
|
+
anchor: MonthAnchor
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
/**
|
|
64
85
|
* A schedule for something that happens on a regular daily/weekly/monthly basis.
|
|
65
86
|
*/
|
|
66
|
-
export type RegularSchedule = EmptySchedule | DailySchedule | WeeklySchedule | MonthlySchedule
|
|
87
|
+
export type RegularSchedule = EmptySchedule | DailySchedule | WeekdailySchedule | WeeklySchedule | MonthlySchedule | MonthAnchoredSchedule
|
|
67
88
|
|
|
68
|
-
export type ScheduleType = '
|
|
89
|
+
export type ScheduleType = RegularSchedule['schedule_type']
|
|
69
90
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
91
|
+
const ScheduleTypeOptions = {
|
|
92
|
+
none: "None",
|
|
93
|
+
daily: "Daily",
|
|
94
|
+
weekdaily: "Every Weekday",
|
|
95
|
+
weekly: "Weekly",
|
|
96
|
+
monthly: "Monthly",
|
|
97
|
+
monthanchored: "Anchored Date",
|
|
98
|
+
} as const satisfies Record<ScheduleType, string>
|
|
79
99
|
|
|
80
100
|
|
|
81
101
|
////////////////////////////////////////////////////////////////////////////////
|
|
82
102
|
// Form
|
|
83
103
|
////////////////////////////////////////////////////////////////////////////////
|
|
84
104
|
|
|
85
|
-
export
|
|
105
|
+
export type RegularScheduleFieldsOptions = {
|
|
106
|
+
// Show the option to select "none" schedule type (default true)
|
|
107
|
+
showNoneOption?: boolean
|
|
108
|
+
// Format the title of each schedule type option
|
|
109
|
+
optionTitle?: (type: ScheduleType, title: string) => string
|
|
110
|
+
}
|
|
86
111
|
|
|
87
|
-
|
|
112
|
+
export class RegularScheduleFields extends TerrierFormFields<RegularSchedule> {
|
|
88
113
|
|
|
89
|
-
|
|
90
|
-
* Set false to not render the 'none' option
|
|
91
|
-
*/
|
|
92
|
-
showNoneOption: boolean = true
|
|
114
|
+
scheduleTypeChangeKey = Messages.typedKey<{ schedule_type: ScheduleType }>()
|
|
93
115
|
|
|
94
|
-
constructor(part: TerrierPart<any>, data:
|
|
116
|
+
constructor(part: TerrierPart<any>, data: RegularSchedule, public options: RegularScheduleFieldsOptions = {}) {
|
|
95
117
|
super(part, data)
|
|
96
118
|
|
|
119
|
+
this.options.showNoneOption ??= true
|
|
120
|
+
|
|
97
121
|
this.part.onChange(this.scheduleTypeChangeKey, m => {
|
|
98
122
|
log.info(`Schedule type changed to ${m.data.schedule_type}`)
|
|
99
|
-
this.data = m.data
|
|
123
|
+
this.data = m.data as RegularSchedule
|
|
100
124
|
this.part.dirty()
|
|
101
125
|
})
|
|
102
126
|
}
|
|
103
127
|
|
|
104
|
-
get parentClasses(): Array<string> {
|
|
105
|
-
return ['tt-flex', 'column', 'gap', 'regular-schedule-form', 'tt-form']
|
|
106
|
-
}
|
|
107
|
-
|
|
108
128
|
render(parent: PartTag): any {
|
|
109
129
|
parent.div('.tt-flex.column.gap.regular-schedule-form.tt-form', col => {
|
|
110
|
-
if (this.showNoneOption) {
|
|
111
|
-
|
|
112
|
-
this.radio(label, 'schedule_type', 'none')
|
|
113
|
-
.emitChange(this.scheduleTypeChangeKey, { schedule_type: 'none' })
|
|
114
|
-
label.span().text("Do Not Deliver")
|
|
115
|
-
})
|
|
130
|
+
if (this.options.showNoneOption) {
|
|
131
|
+
this.renderSection(col, 'none')
|
|
116
132
|
}
|
|
117
133
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
this.select(row, 'hour_of_day', HourOfDayOptions)
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
col.label('.caption-size', label => {
|
|
130
|
-
this.radio(label, 'schedule_type', 'weekly')
|
|
131
|
-
.emitChange(this.scheduleTypeChangeKey, {schedule_type: 'weekly'})
|
|
132
|
-
label.span().text("Deliver Weekly")
|
|
133
|
-
})
|
|
134
|
-
if (this.data.schedule_type == 'weekly') {
|
|
135
|
-
col.div('.schedule-type-fields.weekly.tt-flex.gap', row => {
|
|
136
|
-
this.select(row, 'day_of_week', DayOfWeekOptions)
|
|
137
|
-
.data({tooltip: "Day of the week"})
|
|
138
|
-
this.select(row, 'hour_of_day', HourOfDayOptions)
|
|
139
|
-
})
|
|
140
|
-
}
|
|
134
|
+
this.renderSection(col, 'daily')
|
|
135
|
+
this.renderSection(col, 'weekdaily')
|
|
136
|
+
this.renderSection(col, 'weekly')
|
|
137
|
+
this.renderSection(col, 'monthly')
|
|
138
|
+
this.renderSection(col, 'monthanchored')
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (this.
|
|
148
|
-
|
|
149
|
-
this.select(row, 'day_of_month', DayOfMonthOptions)
|
|
150
|
-
.data({tooltip: "Day of the month"})
|
|
151
|
-
this.select(row, 'hour_of_day', HourOfDayOptions)
|
|
152
|
-
})
|
|
142
|
+
private renderSection(parent: PartTag, scheduleType: ScheduleType): void {
|
|
143
|
+
parent.label('.caption-size', label => {
|
|
144
|
+
this.radio(label, 'schedule_type', scheduleType)
|
|
145
|
+
.emitChange(this.scheduleTypeChangeKey, { schedule_type: scheduleType })
|
|
146
|
+
let optionTitle: string = ScheduleTypeOptions[scheduleType]
|
|
147
|
+
if (this.options.optionTitle) {
|
|
148
|
+
optionTitle = this.options.optionTitle(scheduleType, optionTitle)
|
|
153
149
|
}
|
|
150
|
+
label.span().text(optionTitle)
|
|
154
151
|
})
|
|
152
|
+
if (scheduleType != 'none' && this.data.schedule_type == scheduleType) {
|
|
153
|
+
parent.div(`.schedule-type-fields.tt-flex.small-gap.align-center.shrink-items`, row => {
|
|
154
|
+
row.class(scheduleType)
|
|
155
|
+
|
|
156
|
+
switch (scheduleType) {
|
|
157
|
+
case "daily":
|
|
158
|
+
case "weekdaily":
|
|
159
|
+
this.renderDailyFields(row, this as TerrierFormFields<DailySchedule>)
|
|
160
|
+
break
|
|
161
|
+
case "weekly":
|
|
162
|
+
this.renderWeeklyFields(row, this as TerrierFormFields<WeeklySchedule>)
|
|
163
|
+
break
|
|
164
|
+
case "monthly":
|
|
165
|
+
this.renderMonthlyFields(row, this as TerrierFormFields<MonthlySchedule>)
|
|
166
|
+
break
|
|
167
|
+
case "monthanchored":
|
|
168
|
+
this.renderMonthAnchoredFields(row, this as TerrierFormFields<MonthAnchoredSchedule>)
|
|
169
|
+
break
|
|
170
|
+
default:
|
|
171
|
+
unreachable(scheduleType)
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private renderDailyFields(parent: PartTag, formFields: TerrierFormFields<DailySchedule>): void {
|
|
178
|
+
formFields.select(parent, 'hour_of_day', HourOfDayOptions)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private renderWeeklyFields(parent: PartTag, formFields: TerrierFormFields<WeeklySchedule>): void {
|
|
182
|
+
parent.span().text("Every")
|
|
183
|
+
formFields.select(parent.div(), 'day_of_week', DayOfWeekOptions)
|
|
184
|
+
.data({ tooltip: "Day of the week" })
|
|
185
|
+
parent.span().text("at")
|
|
186
|
+
formFields.select(parent.div(), 'hour_of_day', HourOfDayOptions)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private renderMonthlyFields(parent: PartTag, formFields: TerrierFormFields<MonthlySchedule>): void {
|
|
190
|
+
formFields.select(parent.div(), 'day_of_month', DayOfMonthOptions)
|
|
191
|
+
.data({ tooltip: "Day of the month" })
|
|
192
|
+
parent.span().text("of every month at")
|
|
193
|
+
formFields.select(parent.div(), 'hour_of_day', HourOfDayOptions)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private renderMonthAnchoredFields(parent: PartTag, formFields: TerrierFormFields<MonthAnchoredSchedule>): void {
|
|
197
|
+
formFields.select(parent.div(), 'anchor', MonthAnchorOptions)
|
|
198
|
+
parent.span().text("of every month at")
|
|
199
|
+
formFields.select(parent.div(), 'hour_of_day', HourOfDayOptions)
|
|
155
200
|
}
|
|
156
201
|
|
|
157
202
|
|
|
@@ -161,19 +206,26 @@ export class RegularScheduleFields extends TerrierFormFields<CombinedRegularSche
|
|
|
161
206
|
async serializeConcrete(): Promise<RegularSchedule> {
|
|
162
207
|
const raw = await this.serialize()
|
|
163
208
|
const schedule_type = raw.schedule_type
|
|
164
|
-
|
|
209
|
+
|
|
165
210
|
log.info(`Serializing schedule type ${schedule_type}`, raw)
|
|
211
|
+
|
|
212
|
+
if (schedule_type == 'none') {
|
|
213
|
+
return { schedule_type }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const hour_of_day = raw.hour_of_day ?? '0'
|
|
166
217
|
switch (schedule_type) {
|
|
167
|
-
case 'none':
|
|
168
|
-
return {schedule_type}
|
|
169
218
|
case 'daily':
|
|
170
|
-
|
|
219
|
+
case 'weekdaily':
|
|
220
|
+
return { schedule_type, hour_of_day }
|
|
171
221
|
case 'weekly':
|
|
172
|
-
return {schedule_type, hour_of_day, day_of_week: raw.day_of_week
|
|
222
|
+
return { schedule_type, hour_of_day, day_of_week: raw.day_of_week ?? 'sunday' }
|
|
173
223
|
case 'monthly':
|
|
174
|
-
return {schedule_type, hour_of_day, day_of_month: raw.day_of_month
|
|
224
|
+
return { schedule_type, hour_of_day, day_of_month: raw.day_of_month ?? '1' }
|
|
225
|
+
case 'monthanchored':
|
|
226
|
+
return { schedule_type, hour_of_day, anchor: raw.anchor ?? 'first_day' }
|
|
175
227
|
default:
|
|
176
|
-
|
|
228
|
+
unreachable(schedule_type)
|
|
177
229
|
}
|
|
178
230
|
}
|
|
179
231
|
|
|
@@ -188,19 +240,26 @@ export class RegularScheduleFields extends TerrierFormFields<CombinedRegularSche
|
|
|
188
240
|
* Generate an english description of the given regular schedule.
|
|
189
241
|
* @param schedule
|
|
190
242
|
*/
|
|
191
|
-
function describeRegular(schedule:
|
|
192
|
-
|
|
243
|
+
function describeRegular(schedule: RegularSchedule): string {
|
|
244
|
+
if (schedule.schedule_type == 'none') {
|
|
245
|
+
return "Unscheduled"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const timeString = dayjs().hour(parseInt(schedule.hour_of_day ?? '0')).format('h A')
|
|
193
249
|
switch (schedule.schedule_type) {
|
|
194
|
-
case 'none':
|
|
195
|
-
return "Unscheduled"
|
|
196
250
|
case 'daily':
|
|
197
251
|
return `Daily at ${timeString}`
|
|
252
|
+
case 'weekdaily':
|
|
253
|
+
return `Every Weekday at ${timeString}`
|
|
198
254
|
case 'weekly':
|
|
199
|
-
return `Every ${inflection.titleize(schedule.day_of_week
|
|
255
|
+
return `Every ${inflection.titleize(schedule.day_of_week ?? 'sunday')} at ${timeString}`
|
|
200
256
|
case 'monthly':
|
|
201
|
-
return `
|
|
257
|
+
return `The ${inflection.ordinalize(schedule.day_of_month ?? '1')} of every month at ${timeString}`
|
|
258
|
+
case 'monthanchored':
|
|
259
|
+
const anchor = MonthAnchors[schedule.anchor ?? 'first_day'].toLocaleLowerCase()
|
|
260
|
+
return `The ${anchor} of every month at ${timeString}`
|
|
202
261
|
default:
|
|
203
|
-
|
|
262
|
+
unreachable(schedule)
|
|
204
263
|
}
|
|
205
264
|
}
|
|
206
265
|
|
package/terrier/utils.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A function to assert at compile time that this execution branch cannot happen because all possible types of `x`
|
|
3
|
+
* have been exhausted.
|
|
4
|
+
*
|
|
5
|
+
* If the compiler thinks that the value `x` has any valid type, this will raise a compilation error.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // if `Thing` changes (e.g., `Charlie` is added), the call to `unreachable` will fail compilation with
|
|
9
|
+
* // the error message "Argument of type Charlie is not assignable to parameter of type never".
|
|
10
|
+
* type Alpha = { type: 'alpha' }
|
|
11
|
+
* type Bravo = { type: 'bravo' }
|
|
12
|
+
* type Thing = Alpha | Bravo
|
|
13
|
+
* function switchOnThing(thing: Thing) {
|
|
14
|
+
* switch (thing.type) {
|
|
15
|
+
* case 'alpha':
|
|
16
|
+
* console.log("Alpha happened!")
|
|
17
|
+
* return
|
|
18
|
+
* case 'bravo':
|
|
19
|
+
* console.log("Bravo happened!")
|
|
20
|
+
* return
|
|
21
|
+
* default:
|
|
22
|
+
* unreachable(thing)
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* @throws UnreachableException thrown if we reach this point at runtime
|
|
27
|
+
*/
|
|
28
|
+
export function unreachable(x: never): never {
|
|
29
|
+
console.warn("unreachable got value", x)
|
|
30
|
+
throw new UnreachableException()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class UnreachableException extends Error {
|
|
34
|
+
constructor() {
|
|
35
|
+
super("Reached execution point thought to be unreachable at compile time")
|
|
36
|
+
this.name = "UnreachableException"
|
|
37
|
+
}
|
|
38
|
+
}
|