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.
@@ -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
- this.scheduleFields.showNoneOption = false
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-flex.padded.large-gap", row => {
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.padded.column.gap.dd-new-query-form', col => {
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.padded.gap", col => {
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.padded.gap.column', col => {
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.56.2",
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": "^3.2.4"
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
+ }
@@ -1,10 +1,12 @@
1
- import {PartTag} from "tuff-core/parts"
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 {Logger} from "tuff-core/logging"
5
- import dayjs from "dayjs"
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 = 'none' | 'daily' | 'weekly' | 'monthly'
89
+ export type ScheduleType = RegularSchedule['schedule_type']
69
90
 
70
- /**
71
- * All possible variations of RegularSchedule.
72
- */
73
- export type CombinedRegularSchedule = {
74
- schedule_type: ScheduleType
75
- hour_of_day?: HourOfDay
76
- day_of_week?: DayOfWeek
77
- day_of_month?: DayOfMonth
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 class RegularScheduleFields extends TerrierFormFields<CombinedRegularSchedule> {
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
- scheduleTypeChangeKey = Messages.typedKey<{schedule_type: ScheduleType}>()
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: CombinedRegularSchedule) {
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
- col.label('.caption-size', label => {
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
- col.label('.caption-size', label => {
119
- this.radio(label, 'schedule_type', 'daily')
120
- .emitChange(this.scheduleTypeChangeKey, {schedule_type: 'daily'})
121
- label.span().text("Deliver Daily")
122
- })
123
- if (this.data.schedule_type == 'daily') {
124
- col.div('.schedule-type-fields.daily.tt-flex.gap', row => {
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
- col.label('.caption-size', label => {
143
- this.radio(label, 'schedule_type', 'monthly')
144
- .emitChange(this.scheduleTypeChangeKey, {schedule_type: 'monthly'})
145
- label.span().text("Deliver Monthly")
146
- })
147
- if (this.data.schedule_type == 'monthly') {
148
- col.div('.schedule-type-fields.monthly.tt-flex.gap', row => {
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
- const hour_of_day = raw.hour_of_day || '0'
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
- return {schedule_type, hour_of_day}
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 || 'sunday'}
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 || '1'}
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
- throw `Invalid schedule type: ${schedule_type}`
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: CombinedRegularSchedule): string {
192
- const timeString = dayjs().hour(parseInt(schedule.hour_of_day || '0')).format('h A')
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 || 'sunday')} at ${timeString}`
255
+ return `Every ${inflection.titleize(schedule.day_of_week ?? 'sunday')} at ${timeString}`
200
256
  case 'monthly':
201
- return `Every ${inflection.ordinalize(schedule.day_of_month || '1')} of the month at ${timeString}`
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
- throw `Invalid schedule type: ${schedule.schedule_type}`
262
+ unreachable(schedule)
204
263
  }
205
264
  }
206
265
 
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Simplifies a complex compound type into a flat object.
3
+ */
4
+ export type Simplify<T> = {[KeyType in keyof T]: T[KeyType]} & {};
@@ -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
+ }