terrier-engine 4.1.0 → 4.3.3

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.
Files changed (291) hide show
  1. package/api.ts +120 -1
  2. package/app.ts +65 -13
  3. package/attachments.ts +55 -0
  4. package/dropdowns.ts +6 -17
  5. package/format.ts +24 -0
  6. package/forms.ts +52 -1
  7. package/fragments.ts +42 -30
  8. package/gen/hub-icons.ts +697 -0
  9. package/glyps.ts +2 -2
  10. package/ids.ts +12 -0
  11. package/images/icons/active.svg +1 -0
  12. package/images/icons/admin.svg +1 -0
  13. package/images/icons/archive.svg +1 -0
  14. package/images/icons/arrow_down.svg +1 -0
  15. package/images/icons/arrow_left.svg +1 -0
  16. package/images/icons/arrow_right.svg +1 -0
  17. package/images/icons/arrow_up.svg +1 -0
  18. package/images/icons/assign.svg +1 -0
  19. package/images/icons/attachment.svg +1 -0
  20. package/images/icons/back.svg +1 -0
  21. package/images/icons/badge.svg +1 -0
  22. package/images/icons/board.svg +1 -0
  23. package/images/icons/branch.svg +1 -0
  24. package/images/icons/bug.svg +1 -0
  25. package/images/icons/calculator.svg +1 -0
  26. package/images/icons/checkmark.svg +1 -0
  27. package/images/icons/close.svg +1 -0
  28. package/images/icons/clypboard.svg +1 -0
  29. package/images/icons/comment.svg +1 -0
  30. package/images/icons/complete.svg +1 -0
  31. package/images/icons/dashboard.svg +1 -0
  32. package/images/icons/data_pull.svg +1 -0
  33. package/images/icons/data_update.svg +1 -0
  34. package/images/icons/database.svg +1 -0
  35. package/images/icons/day.svg +1 -0
  36. package/images/icons/delete.svg +1 -0
  37. package/images/icons/documentation.svg +1 -0
  38. package/images/icons/edit.svg +1 -0
  39. package/images/icons/feature.svg +1 -0
  40. package/images/icons/flex.svg +1 -0
  41. package/images/icons/forward.svg +1 -0
  42. package/images/icons/github.svg +1 -0
  43. package/images/icons/history.svg +1 -0
  44. package/images/icons/home.svg +1 -0
  45. package/images/icons/image.svg +1 -0
  46. package/images/icons/inbox.svg +1 -0
  47. package/images/icons/info.svg +1 -0
  48. package/images/icons/issue.svg +1 -0
  49. package/images/icons/lane.svg +1 -0
  50. package/images/icons/lane_asap.svg +1 -0
  51. package/images/icons/lane_days.svg +1 -0
  52. package/images/icons/lane_hours.svg +1 -0
  53. package/images/icons/lane_weeks.svg +1 -0
  54. package/images/icons/lanes_board.svg +1 -0
  55. package/images/icons/level_complete.svg +1 -0
  56. package/images/icons/level_highway.svg +1 -0
  57. package/images/icons/level_on_ramp.svg +1 -0
  58. package/images/icons/level_parking.svg +1 -0
  59. package/images/icons/minus.svg +1 -0
  60. package/images/icons/night.svg +1 -0
  61. package/images/icons/origin.svg +1 -0
  62. package/images/icons/pending.svg +1 -0
  63. package/images/icons/plus.svg +1 -0
  64. package/images/icons/post.svg +1 -0
  65. package/images/icons/pr_closed.svg +1 -0
  66. package/images/icons/pr_merged.svg +1 -0
  67. package/images/icons/pr_open.svg +1 -0
  68. package/images/icons/prioritized.svg +1 -0
  69. package/images/icons/project.svg +1 -0
  70. package/images/icons/question.svg +1 -0
  71. package/images/icons/reaction.svg +1 -0
  72. package/images/icons/recent.svg +1 -0
  73. package/images/icons/refresh.svg +1 -0
  74. package/images/icons/request.svg +1 -0
  75. package/images/icons/settings.svg +1 -0
  76. package/images/icons/status.svg +1 -0
  77. package/images/icons/step_deploy.svg +1 -0
  78. package/images/icons/step_develop.svg +1 -0
  79. package/images/icons/step_investigate.svg +1 -0
  80. package/images/icons/step_review.svg +1 -0
  81. package/images/icons/step_test.svg +1 -0
  82. package/images/icons/steps.svg +1 -0
  83. package/images/icons/steps_board.svg +1 -0
  84. package/images/icons/subscribe.svg +1 -0
  85. package/images/icons/support.svg +1 -0
  86. package/images/icons/terrier.svg +1 -0
  87. package/images/icons/thumbs_up.svg +1 -0
  88. package/images/icons/type.svg +1 -0
  89. package/images/icons/unprioritized.svg +1 -0
  90. package/images/icons/upload.svg +1 -0
  91. package/images/icons/user.svg +1 -0
  92. package/images/icons/users.svg +1 -0
  93. package/images/optimized/icon-active.svg +1 -0
  94. package/images/optimized/icon-admin.svg +1 -0
  95. package/images/optimized/icon-archive.svg +1 -0
  96. package/images/optimized/icon-arrow_down.svg +1 -0
  97. package/images/optimized/icon-arrow_left.svg +1 -0
  98. package/images/optimized/icon-arrow_right.svg +1 -0
  99. package/images/optimized/icon-arrow_up.svg +1 -0
  100. package/images/optimized/icon-assign.svg +1 -0
  101. package/images/optimized/icon-attachment.svg +1 -0
  102. package/images/optimized/icon-back.svg +1 -0
  103. package/images/optimized/icon-badge.svg +1 -0
  104. package/images/optimized/icon-board.svg +1 -0
  105. package/images/optimized/icon-branch.svg +1 -0
  106. package/images/optimized/icon-bug.svg +1 -0
  107. package/images/optimized/icon-calculator.svg +1 -0
  108. package/images/optimized/icon-checkmark.svg +1 -0
  109. package/images/optimized/icon-close.svg +1 -0
  110. package/images/optimized/icon-clypboard.svg +1 -0
  111. package/images/optimized/icon-comment.svg +1 -0
  112. package/images/optimized/icon-complete.svg +1 -0
  113. package/images/optimized/icon-dashboard.svg +1 -0
  114. package/images/optimized/icon-data_pull.svg +1 -0
  115. package/images/optimized/icon-data_update.svg +1 -0
  116. package/images/optimized/icon-database.svg +1 -0
  117. package/images/optimized/icon-day.svg +1 -0
  118. package/images/optimized/icon-delete.svg +1 -0
  119. package/images/optimized/icon-documentation.svg +1 -0
  120. package/images/optimized/icon-edit.svg +1 -0
  121. package/images/optimized/icon-feature.svg +1 -0
  122. package/images/optimized/icon-flex.svg +1 -0
  123. package/images/optimized/icon-forward.svg +1 -0
  124. package/images/optimized/icon-github.svg +1 -0
  125. package/images/optimized/icon-history.svg +1 -0
  126. package/images/optimized/icon-home.svg +1 -0
  127. package/images/optimized/icon-image.svg +1 -0
  128. package/images/optimized/icon-inbox.svg +1 -0
  129. package/images/optimized/icon-info.svg +1 -0
  130. package/images/optimized/icon-issue.svg +1 -0
  131. package/images/optimized/icon-lane.svg +1 -0
  132. package/images/optimized/icon-lane_asap.svg +1 -0
  133. package/images/optimized/icon-lane_days.svg +1 -0
  134. package/images/optimized/icon-lane_hours.svg +1 -0
  135. package/images/optimized/icon-lane_weeks.svg +1 -0
  136. package/images/optimized/icon-lanes_board.svg +1 -0
  137. package/images/optimized/icon-level_complete.svg +1 -0
  138. package/images/optimized/icon-level_highway.svg +1 -0
  139. package/images/optimized/icon-level_on_ramp.svg +1 -0
  140. package/images/optimized/icon-level_parking.svg +1 -0
  141. package/images/optimized/icon-minus.svg +1 -0
  142. package/images/optimized/icon-night.svg +1 -0
  143. package/images/optimized/icon-origin.svg +1 -0
  144. package/images/optimized/icon-pending.svg +1 -0
  145. package/images/optimized/icon-plus.svg +1 -0
  146. package/images/optimized/icon-post.svg +1 -0
  147. package/images/optimized/icon-pr_closed.svg +1 -0
  148. package/images/optimized/icon-pr_merged.svg +1 -0
  149. package/images/optimized/icon-pr_open.svg +1 -0
  150. package/images/optimized/icon-prioritized.svg +1 -0
  151. package/images/optimized/icon-project.svg +1 -0
  152. package/images/optimized/icon-question.svg +1 -0
  153. package/images/optimized/icon-reaction.svg +1 -0
  154. package/images/optimized/icon-recent.svg +1 -0
  155. package/images/optimized/icon-refresh.svg +1 -0
  156. package/images/optimized/icon-request.svg +1 -0
  157. package/images/optimized/icon-settings.svg +1 -0
  158. package/images/optimized/icon-status.svg +1 -0
  159. package/images/optimized/icon-step_deploy.svg +1 -0
  160. package/images/optimized/icon-step_develop.svg +1 -0
  161. package/images/optimized/icon-step_investigate.svg +1 -0
  162. package/images/optimized/icon-step_review.svg +1 -0
  163. package/images/optimized/icon-step_test.svg +1 -0
  164. package/images/optimized/icon-steps.svg +1 -0
  165. package/images/optimized/icon-steps_board.svg +1 -0
  166. package/images/optimized/icon-subscribe.svg +1 -0
  167. package/images/optimized/icon-support.svg +1 -0
  168. package/images/optimized/icon-terrier.svg +1 -0
  169. package/images/optimized/icon-thumbs_up.svg +1 -0
  170. package/images/optimized/icon-type.svg +1 -0
  171. package/images/optimized/icon-unprioritized.svg +1 -0
  172. package/images/optimized/icon-upload.svg +1 -0
  173. package/images/optimized/icon-user.svg +1 -0
  174. package/images/optimized/icon-users.svg +1 -0
  175. package/images/optimized/terrier-hub-favicon.svg +1 -0
  176. package/images/optimized/terrier-hub-icon-dark.svg +1 -0
  177. package/images/optimized/terrier-hub-icon-light.svg +1 -0
  178. package/images/optimized/terrier-hub-loader.svg +1 -0
  179. package/images/optimized/terrier-hub-logo-dark.svg +1 -0
  180. package/images/optimized/terrier-hub-logo-light.svg +1 -0
  181. package/images/raw/icon-active.svg +8 -0
  182. package/images/raw/icon-admin.svg +9 -0
  183. package/images/raw/icon-archive.svg +9 -0
  184. package/images/raw/icon-arrow_down.svg +7 -0
  185. package/images/raw/icon-arrow_left.svg +7 -0
  186. package/images/raw/icon-arrow_right.svg +7 -0
  187. package/images/raw/icon-arrow_up.svg +7 -0
  188. package/images/raw/icon-assign.svg +8 -0
  189. package/images/raw/icon-attachment.svg +7 -0
  190. package/images/raw/icon-back.svg +7 -0
  191. package/images/raw/icon-badge.svg +10 -0
  192. package/images/raw/icon-board.svg +20 -0
  193. package/images/raw/icon-branch.svg +11 -0
  194. package/images/raw/icon-bug.svg +8 -0
  195. package/images/raw/icon-calculator.svg +31 -0
  196. package/images/raw/icon-checkmark.svg +8 -0
  197. package/images/raw/icon-close.svg +8 -0
  198. package/images/raw/icon-clypboard.svg +9 -0
  199. package/images/raw/icon-comment.svg +12 -0
  200. package/images/raw/icon-complete.svg +8 -0
  201. package/images/raw/icon-dashboard.svg +18 -0
  202. package/images/raw/icon-data_pull.svg +9 -0
  203. package/images/raw/icon-data_update.svg +9 -0
  204. package/images/raw/icon-database.svg +10 -0
  205. package/images/raw/icon-day.svg +19 -0
  206. package/images/raw/icon-delete.svg +11 -0
  207. package/images/raw/icon-documentation.svg +21 -0
  208. package/images/raw/icon-edit.svg +11 -0
  209. package/images/raw/icon-feature.svg +7 -0
  210. package/images/raw/icon-flex.svg +6 -0
  211. package/images/raw/icon-forward.svg +7 -0
  212. package/images/raw/icon-github.svg +8 -0
  213. package/images/raw/icon-history.svg +12 -0
  214. package/images/raw/icon-home.svg +8 -0
  215. package/images/raw/icon-image.svg +9 -0
  216. package/images/raw/icon-inbox.svg +9 -0
  217. package/images/raw/icon-info.svg +11 -0
  218. package/images/raw/icon-issue.svg +10 -0
  219. package/images/raw/icon-lane.svg +9 -0
  220. package/images/raw/icon-lane_asap.svg +9 -0
  221. package/images/raw/icon-lane_days.svg +11 -0
  222. package/images/raw/icon-lane_hours.svg +8 -0
  223. package/images/raw/icon-lane_weeks.svg +10 -0
  224. package/images/raw/icon-lanes_board.svg +10 -0
  225. package/images/raw/icon-level_complete.svg +8 -0
  226. package/images/raw/icon-level_highway.svg +11 -0
  227. package/images/raw/icon-level_on_ramp.svg +8 -0
  228. package/images/raw/icon-level_parking.svg +10 -0
  229. package/images/raw/icon-minus.svg +8 -0
  230. package/images/raw/icon-night.svg +9 -0
  231. package/images/raw/icon-origin.svg +11 -0
  232. package/images/raw/icon-pending.svg +10 -0
  233. package/images/raw/icon-plus.svg +8 -0
  234. package/images/raw/icon-post.svg +12 -0
  235. package/images/raw/icon-pr_closed.svg +15 -0
  236. package/images/raw/icon-pr_merged.svg +11 -0
  237. package/images/raw/icon-pr_open.svg +12 -0
  238. package/images/raw/icon-prioritized.svg +11 -0
  239. package/images/raw/icon-project.svg +24 -0
  240. package/images/raw/icon-question.svg +9 -0
  241. package/images/raw/icon-reaction.svg +6 -0
  242. package/images/raw/icon-recent.svg +12 -0
  243. package/images/raw/icon-refresh.svg +11 -0
  244. package/images/raw/icon-request.svg +10 -0
  245. package/images/raw/icon-settings.svg +11 -0
  246. package/images/raw/icon-status.svg +8 -0
  247. package/images/raw/icon-step_deploy.svg +15 -0
  248. package/images/raw/icon-step_develop.svg +12 -0
  249. package/images/raw/icon-step_investigate.svg +14 -0
  250. package/images/raw/icon-step_review.svg +11 -0
  251. package/images/raw/icon-step_test.svg +14 -0
  252. package/images/raw/icon-steps.svg +18 -0
  253. package/images/raw/icon-steps_board.svg +19 -0
  254. package/images/raw/icon-subscribe.svg +10 -0
  255. package/images/raw/icon-support.svg +14 -0
  256. package/images/raw/icon-terrier.svg +7 -0
  257. package/images/raw/icon-thumbs_up.svg +1 -0
  258. package/images/raw/icon-type.svg +15 -0
  259. package/images/raw/icon-unprioritized.svg +10 -0
  260. package/images/raw/icon-upload.svg +8 -0
  261. package/images/raw/icon-user.svg +9 -0
  262. package/images/raw/icon-users.svg +14 -0
  263. package/images/raw/terrier-hub-favicon-alert.png +0 -0
  264. package/images/raw/terrier-hub-favicon-dark.png +0 -0
  265. package/images/raw/terrier-hub-favicon.png +0 -0
  266. package/images/raw/terrier-hub-favicon.svg +29 -0
  267. package/images/raw/terrier-hub-icon-dark.svg +23 -0
  268. package/images/raw/terrier-hub-icon-light.png +0 -0
  269. package/images/raw/terrier-hub-icon-light.svg +23 -0
  270. package/images/raw/terrier-hub-loader.svg +54 -0
  271. package/images/raw/terrier-hub-logo-dark.svg +27 -0
  272. package/images/raw/terrier-hub-logo-light.png +0 -0
  273. package/images/raw/terrier-hub-logo-light.svg +28 -0
  274. package/lightbox.ts +8 -21
  275. package/loading.ts +5 -6
  276. package/modals.ts +41 -19
  277. package/overlays.ts +2 -2
  278. package/package.json +4 -2
  279. package/parts/content-part.ts +24 -25
  280. package/parts/not-found-page.ts +1 -7
  281. package/parts/page-part.ts +47 -54
  282. package/parts/panel-part.ts +1 -8
  283. package/parts/terrier-form-part.ts +20 -0
  284. package/parts/terrier-part.ts +51 -13
  285. package/schema.ts +29 -1
  286. package/sheets.ts +100 -0
  287. package/tabs.ts +190 -0
  288. package/theme.ts +41 -12
  289. package/toasts.ts +9 -9
  290. package/tooltips.ts +2 -2
  291. package/parts/themed-form-part.ts +0 -25
package/api.ts CHANGED
@@ -4,6 +4,11 @@ import { QueryParams } from "tuff-core/urls"
4
4
  const log = new Logger('Api')
5
5
  log.level = 'debug'
6
6
 
7
+
8
+ ////////////////////////////////////////////////////////////////////////////////
9
+ // Basic Requests
10
+ ////////////////////////////////////////////////////////////////////////////////
11
+
7
12
  /**
8
13
  * All API responses containing these fields.
9
14
  */
@@ -104,9 +109,123 @@ async function post<ResponseType>(url: string, body: Record<string, unknown> | F
104
109
  }
105
110
 
106
111
 
112
+ ////////////////////////////////////////////////////////////////////////////////
113
+ // Event Streams
114
+ ////////////////////////////////////////////////////////////////////////////////
115
+
116
+ type LogLevel = 'success' | 'info' | 'warn' | 'debug'
117
+
118
+ /**
119
+ * Type of log events from a streaming response.
120
+ */
121
+ export type LogEvent = {
122
+ level: LogLevel
123
+ prefix?: string
124
+ message: string
125
+ }
126
+
127
+ /**
128
+ * Type of error events from a streaming response.
129
+ */
130
+ export type ErrorEvent = {
131
+ prefix?: string
132
+ message: string
133
+ backtrace: string[]
134
+ }
135
+
136
+ /**
137
+ * Configure a `Streamer`.
138
+ */
139
+ export type StreamOptions = {
140
+ keepAlive?: boolean
141
+ }
142
+
143
+ type noArgListener = () => any
144
+
145
+ type StreamLifecycle = 'close'
146
+
147
+ /**
148
+ * Exposes a typesafe API for handling streaming responses using SSE.
149
+ */
150
+ export class Streamer {
151
+
152
+ sse!: EventSource
153
+ lifecycleListeners: Record<StreamLifecycle, noArgListener[]> = {
154
+ close: []
155
+ }
156
+
157
+ constructor(readonly url: string, readonly options: StreamOptions) {
158
+ this.sse = new EventSource(url)
159
+
160
+ // this is a special event sent by the ResponseStreamer on the server
161
+ // to tell us that the request is done
162
+ this.sse.addEventListener('_close', evt => {
163
+ if (!this.options.keepAlive) {
164
+ log.debug(`Closing Streamer at ${url}`, evt)
165
+ this.sse.close()
166
+ for (const listener of this.lifecycleListeners['close']) {
167
+ listener()
168
+ }
169
+ }
170
+ })
171
+ }
172
+
173
+ /**
174
+ * Register a listener for events of the given type.
175
+ * @param type
176
+ * @param listener
177
+ */
178
+ on<T>(type: string, listener: (event: T) => any) {
179
+ this.sse.addEventListener(type, event => {
180
+ const data = JSON.parse(event.data) as T
181
+ log.debug(`${type} event`, data)
182
+ listener(data)
183
+ })
184
+ return this
185
+ }
186
+
187
+ /**
188
+ * Listen specifically for log events.
189
+ * @param listener
190
+ */
191
+ onLog(listener: (event: LogEvent) => any) {
192
+ return this.on<LogEvent>('_log', listener)
193
+ }
194
+
195
+ /**
196
+ * Listen specifically for error events.
197
+ * @param listener
198
+ */
199
+ onError(listener: (event: ErrorEvent) => any) {
200
+ return this.on<ErrorEvent>('_error', listener)
201
+ }
202
+
203
+ onClose(listener: noArgListener) {
204
+ this.lifecycleListeners['close'].push(listener)
205
+ }
206
+ }
207
+
208
+
209
+
210
+ /**
211
+ * Creates a streaming response for the given endpoint.
212
+ * @param url
213
+ * @param options pass `keepAlive: true` to keep retrying the request
214
+ * @return a `Streamer` on which you attach event handlers
215
+ */
216
+ function stream(url: string, options: StreamOptions={}): Streamer {
217
+ return new Streamer(url, options)
218
+ }
219
+
220
+
221
+ ////////////////////////////////////////////////////////////////////////////////
222
+ // Export
223
+ ////////////////////////////////////////////////////////////////////////////////
224
+
107
225
  const Api = {
108
226
  safeGet,
109
227
  safePost,
110
- post
228
+ post,
229
+ stream
111
230
  }
112
231
  export default Api
package/app.ts CHANGED
@@ -3,12 +3,14 @@ import {Part, PartConstructor, PartParent} from "tuff-core/parts"
3
3
  import TerrierPart from "./parts/terrier-part"
4
4
  import Tooltips from "./tooltips"
5
5
  import Lightbox from "./lightbox"
6
- import Theme, {ThemeType} from "./theme"
6
+ import Theme from "./theme"
7
7
  import {ModalPart, ModalStackPart} from "./modals"
8
8
  import {OverlayLayerType, OverlayPart} from "./overlays"
9
9
 
10
10
  // @ts-ignore
11
11
  import logoUrl from './images/optimized/terrier-hub-logo-light.svg'
12
+ import Sheets, {AlertSheetState, ConfirmSheetState, Sheet, SheetState} from "./sheets";
13
+ import {messages} from "tuff-core";
12
14
 
13
15
  const log = new Logger('App')
14
16
  Logger.level = 'info'
@@ -16,22 +18,18 @@ Logger.level = 'info'
16
18
  /**
17
19
  * Main application part that renders the entire page.
18
20
  */
19
- export abstract class TerrierApp<
20
- TThemeType extends ThemeType,
21
- TSelf extends TerrierApp<TThemeType, TSelf, TTheme>,
22
- TTheme extends Theme<TThemeType>
23
- > extends TerrierPart<{theme: TTheme}, TThemeType, TSelf, TTheme> {
21
+ export abstract class TerrierApp<TState> extends TerrierPart<TState> {
24
22
 
25
- _theme!: TTheme
26
-
27
- get theme(): TTheme {
23
+ _theme!: Theme
24
+
25
+ get theme(): Theme {
28
26
  return this._theme
29
27
  }
30
28
 
31
29
  overlayPart!: OverlayPart
32
30
 
33
31
  async init() {
34
- this._theme = this.state.theme
32
+ this._theme = new Theme()
35
33
  this.overlayPart = this.makePart(OverlayPart, {})
36
34
  log.info("Initialized")
37
35
  }
@@ -44,7 +42,7 @@ export abstract class TerrierApp<
44
42
  update(root: HTMLElement) {
45
43
  log.info(`Update`, root)
46
44
  Tooltips.init(root)
47
- Lightbox.init<TThemeType, TSelf, TTheme>(root, this as unknown as TSelf, 'body-content')
45
+ Lightbox.init(root, this, 'body-content')
48
46
  }
49
47
 
50
48
 
@@ -70,7 +68,7 @@ export abstract class TerrierApp<
70
68
  this.overlayPart.clearAll()
71
69
  }
72
70
 
73
- removeDropdown<StateType extends {}>(state: StateType): boolean {
71
+ removeDropdown<StateType>(state: StateType): boolean {
74
72
  this.lastDropdownTarget = undefined
75
73
  return this.overlayPart.removeLayer(state)
76
74
  }
@@ -86,7 +84,7 @@ export abstract class TerrierApp<
86
84
 
87
85
  /// Modals
88
86
 
89
- showModal<ModalType extends ModalPart<StateType, TThemeType, TSelf, TTheme>, StateType>(
87
+ showModal<ModalType extends ModalPart<StateType>, StateType>(
90
88
  constructor: PartConstructor<ModalType, StateType>,
91
89
  state: StateType
92
90
  ): ModalType {
@@ -97,4 +95,58 @@ export abstract class TerrierApp<
97
95
  }
98
96
 
99
97
 
98
+ /// Sheets
99
+
100
+ /**
101
+ * Shows a confirm sheet to the user, asking them a question
102
+ * @param options
103
+ * @param callback gets called if the user hits "Confirm"
104
+ */
105
+ confirm(options: ConfirmSheetState, callback: () => any) {
106
+ const key = messages.untypedKey()
107
+ const state = {...options,
108
+ primaryActions: [
109
+ {
110
+ title: 'Confirm',
111
+ icon: 'glyp-checkmark',
112
+ click: {key}
113
+ }
114
+ ],
115
+ secondaryActions: [
116
+ {
117
+ title: 'Cancel',
118
+ icon: 'glyp-close',
119
+ classes: ['secondary'],
120
+ click: {key: Sheets.clearKey}
121
+ }
122
+ ]
123
+ } as SheetState
124
+ const sheet = this.overlayPart.getOrCreateLayer(Sheet<SheetState>, state, 'sheet')
125
+ sheet.onClick(key, _ => {
126
+ sheet.clear()
127
+ callback()
128
+ })
129
+ sheet.dirty()
130
+ }
131
+
132
+ /**
133
+ * Shows an alert sheet to the user with a message (but no choices).
134
+ * @param options
135
+ */
136
+ alert(options: AlertSheetState) {
137
+ const state = {...options,
138
+ primaryActions: [
139
+ {
140
+ title: 'Okay',
141
+ icon: 'glyp-checkmark',
142
+ click: {key: Sheets.clearKey},
143
+ classes: ['secondary']
144
+ }
145
+ ]
146
+ } as SheetState
147
+ const sheet = this.overlayPart.getOrCreateLayer(Sheet<SheetState>, state, 'sheet')
148
+ sheet.dirty()
149
+ }
150
+
151
+
100
152
  }
package/attachments.ts ADDED
@@ -0,0 +1,55 @@
1
+ import {Logger} from "tuff-core/logging"
2
+
3
+ const log = new Logger('Attachments')
4
+
5
+ type MetaData = {
6
+ size: number
7
+ filename: string
8
+ mime_type: string
9
+ }
10
+
11
+ type ShrineAttachment = {
12
+ id: string
13
+ storage: string
14
+ metadata: MetaData
15
+ path?: string // used to send tmp filepath to server
16
+ }
17
+
18
+ type Derivatives = {
19
+ thumbnail?: ShrineAttachment
20
+ }
21
+
22
+ export type Attachment = ShrineAttachment & { derivatives?: Derivatives }
23
+
24
+ /**
25
+ * Constructs a url pointing to the original or a derivative of the attachment
26
+ * @param attachment
27
+ * @param derivative
28
+ */
29
+ function url(attachment: Attachment, derivative: keyof Derivatives | null) {
30
+ let path = '/uploads'
31
+ let id
32
+
33
+ if (derivative?.length && attachment.derivatives) {
34
+ const derivativeId = attachment.derivatives[derivative]?.id
35
+ if (derivativeId?.length) {
36
+ path += '/permanent'
37
+ id = derivativeId
38
+ } else {
39
+ log.info(`${derivative} not found, returning original`)
40
+ }
41
+ }
42
+
43
+ if (!id) {
44
+ path += '/cache'
45
+ id = attachment.id
46
+ }
47
+
48
+ return `${path}/${id}`
49
+ }
50
+
51
+ const Attachments = {
52
+ url
53
+ }
54
+
55
+ export default Attachments
package/dropdowns.ts CHANGED
@@ -1,12 +1,11 @@
1
- import { Logger } from "tuff-core/logging"
2
- import { untypedKey } from "tuff-core/messages"
3
- import { unique } from "tuff-core/arrays"
1
+ import {Logger} from "tuff-core/logging"
2
+ import {untypedKey} from "tuff-core/messages"
3
+ import {unique} from "tuff-core/arrays"
4
4
  import {PartTag, StatelessPart} from "tuff-core/parts"
5
5
  import Overlays from "./overlays"
6
6
  import TerrierPart from "./parts/terrier-part"
7
7
  import Objects from "tuff-core/objects"
8
- import Theme, {Action, ThemeType} from "./theme"
9
- import {TerrierApp} from "./app"
8
+ import {Action} from "./theme"
10
9
 
11
10
  const log = new Logger('Dropdowns')
12
11
 
@@ -16,12 +15,7 @@ const clearDropdownKey = untypedKey()
16
15
  * Abstract base class for dropdown parts.
17
16
  * Subclasses must implement the `renderContent()` method to render the dropdown content.
18
17
  */
19
- export abstract class Dropdown<
20
- TState extends {},
21
- TThemeType extends ThemeType,
22
- TApp extends TerrierApp<TThemeType, TApp, TTheme>,
23
- TTheme extends Theme<TThemeType>
24
- > extends TerrierPart<TState, TThemeType, TApp, TTheme> {
18
+ export abstract class Dropdown<TState> extends TerrierPart<TState> {
25
19
 
26
20
  parentPart?: StatelessPart
27
21
 
@@ -96,12 +90,7 @@ export abstract class Dropdown<
96
90
  /**
97
91
  * A concrete dropdown part that shows a list of actions.
98
92
  */
99
- export class ActionsDropdown<
100
- TThemeType extends ThemeType,
101
- TApp extends TerrierApp<TThemeType, TApp, TTheme>,
102
- TTheme extends Theme<TThemeType>
103
- > extends Dropdown<Array<Action<TThemeType>>, TThemeType, TApp, TTheme> {
104
-
93
+ export class ActionsDropdown extends Dropdown<Array<Action>> {
105
94
 
106
95
  get autoClose(): boolean {
107
96
  return true
package/format.ts ADDED
@@ -0,0 +1,24 @@
1
+
2
+ ////////////////////////////////////////////////////////////////////////////////
3
+ // Currency
4
+ ////////////////////////////////////////////////////////////////////////////////
5
+
6
+ /**
7
+ * Formats a cents value as a dollars string.
8
+ * @param c an integer number of cents
9
+ */
10
+ function cents(c: number | string): string {
11
+ const num = typeof c == 'number' ? c : parseInt(c)
12
+ return '$' + (num/100).toFixed(2)
13
+ }
14
+
15
+
16
+ ////////////////////////////////////////////////////////////////////////////////
17
+ // Export
18
+ ////////////////////////////////////////////////////////////////////////////////
19
+
20
+ const Format = {
21
+ cents
22
+ }
23
+
24
+ export default Format
package/forms.ts CHANGED
@@ -1,5 +1,10 @@
1
- import {SelectOptions} from "tuff-core/forms"
1
+ import {Field, FormFields, FormPartData, InputType, KeyOfType, SelectOptions} from "tuff-core/forms"
2
2
  import {strings} from "tuff-core"
3
+ import {DbErrors} from "./db-client"
4
+ import {PartTag} from "tuff-core/parts"
5
+ import {InputTag, InputTagAttrs} from "tuff-core/html"
6
+ import TerrierPart from "./parts/terrier-part";
7
+ import {an} from "vitest/dist/types-ad1c3f45";
3
8
 
4
9
  ////////////////////////////////////////////////////////////////////////////////
5
10
  // Options
@@ -20,6 +25,52 @@ function titleizeOptions(opts: string[], blank?: string): SelectOptions {
20
25
  }
21
26
 
22
27
 
28
+ ////////////////////////////////////////////////////////////////////////////////
29
+ // Form Fields
30
+ ////////////////////////////////////////////////////////////////////////////////
31
+
32
+ /**
33
+ * Override regular `FormFields` methods to include validation errors.
34
+ */
35
+ export class TerrierFormFields<T extends FormPartData> extends FormFields<T> {
36
+
37
+ errors?: DbErrors<T>
38
+
39
+ /**
40
+ * You must pass a `TerrierPart` so that we can render the error bubble with it.
41
+ * @param part
42
+ * @param data
43
+ * @param errors
44
+ */
45
+ constructor(part: TerrierPart<any>, data: T, errors?: DbErrors<T>) {
46
+ super(part, data)
47
+ this.errors = errors
48
+ }
49
+
50
+ protected input<Key extends KeyOfType<T, any> & string>(parent: PartTag, type: InputType, name: Key, serializerType: {
51
+ new(name: string): Field<any, Element>
52
+ }, attrs?: InputTagAttrs): InputTag {
53
+ if (this.errors && this.errors[name]) {
54
+ attrs ||= {}
55
+ attrs.classes ||= []
56
+ attrs.classes.push('error')
57
+ }
58
+ return super.input(parent, type, name, serializerType, attrs);
59
+ }
60
+
61
+ /**
62
+ * Only renders the error bubble if the errors are set.
63
+ * @param parent
64
+ */
65
+ renderErrorBubble(parent: PartTag) {
66
+ if (this.errors) {
67
+ (this.part as TerrierPart<an>).renderErrorBubble(parent, this.errors)
68
+ }
69
+ }
70
+ }
71
+
72
+
73
+
23
74
  ////////////////////////////////////////////////////////////////////////////////
24
75
  // Export
25
76
  ////////////////////////////////////////////////////////////////////////////////
package/fragments.ts CHANGED
@@ -1,14 +1,26 @@
1
1
  import {PartTag} from "tuff-core/parts"
2
2
  import {AnchorTagAttrs, HtmlParentTag} from "tuff-core/html"
3
- import Theme, {Action, Packet, ThemeType} from "./theme"
3
+ import Theme, {Action, ColorName, IconName, Packet} from "./theme"
4
4
  import {ActionLevel, PanelActions} from "./parts/content-part"
5
5
 
6
6
  /**
7
7
  * Base class for Panel and Card fragment builders.
8
8
  */
9
- abstract class ContentFragment<TT extends ThemeType> {
10
- protected constructor(readonly prefix: string, readonly theme: Theme<TT>) {
9
+ abstract class ContentFragment {
10
+ protected constructor(readonly prefix: string, readonly theme: Theme) {
11
+
12
+ }
13
+
11
14
 
15
+ protected _classes: string[] = []
16
+
17
+ /**
18
+ * Add arbitrary classes to the top-level fragment.
19
+ * @param c
20
+ */
21
+ classes(...c: string[]) {
22
+ this._classes.push(...c)
23
+ return this
12
24
  }
13
25
 
14
26
  protected _title?: string
@@ -17,18 +29,18 @@ abstract class ContentFragment<TT extends ThemeType> {
17
29
  * @param t the title
18
30
  * @param icon the optional icon
19
31
  */
20
- title(t: string, icon?: TT['icons']) {
32
+ title(t: string, icon?: IconName) {
21
33
  this._title = t
22
34
  this._icon = icon
23
35
  return this
24
36
  }
25
37
 
26
- protected _icon?: TT['icons']
38
+ protected _icon?: IconName
27
39
 
28
40
  /**
29
41
  * @param i the panel icon
30
42
  */
31
- icon(i: TT['icons']) {
43
+ icon(i: IconName) {
32
44
  this._icon = i
33
45
  return this
34
46
  }
@@ -46,17 +58,17 @@ abstract class ContentFragment<TT extends ThemeType> {
46
58
  }
47
59
 
48
60
 
49
- export class PanelFragment<TT extends ThemeType> extends ContentFragment<TT> {
50
- constructor(theme: Theme<TT>) {
61
+ export class PanelFragment extends ContentFragment {
62
+ constructor(theme: Theme) {
51
63
  super('tt-panel', theme)
52
64
  }
53
65
 
54
66
  /// Actions
55
67
 
56
68
  actions = {
57
- primary: Array<Action<TT>>(),
58
- secondary: Array<Action<TT>>(),
59
- tertiary: Array<Action<TT>>()
69
+ primary: Array<Action>(),
70
+ secondary: Array<Action>(),
71
+ tertiary: Array<Action>()
60
72
  }
61
73
 
62
74
  /**
@@ -64,7 +76,7 @@ export class PanelFragment<TT extends ThemeType> extends ContentFragment<TT> {
64
76
  * @param action the action to add
65
77
  * @param level whether it's a primary, secondary, or tertiary action
66
78
  */
67
- addAction(action: Action<TT>, level: ActionLevel = 'primary') {
79
+ addAction(action: Action, level: ActionLevel = 'primary') {
68
80
  this.actions[level].push(action)
69
81
  return this
70
82
  }
@@ -93,7 +105,7 @@ export class PanelFragment<TT extends ThemeType> extends ContentFragment<TT> {
93
105
  }
94
106
  })
95
107
  panelActions(panel, this.actions, this.theme)
96
- })
108
+ }).class(...this._classes)
97
109
  }
98
110
 
99
111
  }
@@ -104,7 +116,7 @@ export class PanelFragment<TT extends ThemeType> extends ContentFragment<TT> {
104
116
  * @param actions the actions
105
117
  * @param theme the theme with which to render actions
106
118
  */
107
- function panelActions<TT extends ThemeType>(panel: PartTag, actions: PanelActions<TT>, theme: Theme<TT>) {
119
+ function panelActions(panel: PartTag, actions: PanelActions, theme: Theme) {
108
120
  if (actions.primary.length || actions.secondary.length) {
109
121
  panel.div('.panel-actions', actionsContainer => {
110
122
  for (const level of ['secondary', 'primary'] as const) {
@@ -122,8 +134,8 @@ function panelActions<TT extends ThemeType>(panel: PartTag, actions: PanelAction
122
134
  /**
123
135
  * Cards are like panels except they don't have any actions and are themselves an anchor.
124
136
  */
125
- class CardFragment<TT extends ThemeType> extends ContentFragment<TT> {
126
- constructor(theme: Theme<TT>) {
137
+ class CardFragment extends ContentFragment {
138
+ constructor(theme: Theme) {
127
139
  super('tt-card', theme)
128
140
  }
129
141
 
@@ -164,15 +176,15 @@ class CardFragment<TT extends ThemeType> extends ContentFragment<TT> {
164
176
  }
165
177
 
166
178
 
167
- class LabeledValueFragment<TT extends ThemeType> extends ContentFragment<TT> {
179
+ class LabeledValueFragment extends ContentFragment {
168
180
 
169
- constructor(theme: Theme<TT>) {
181
+ constructor(theme: Theme) {
170
182
  super('tt-labeled-value', theme)
171
183
  }
172
184
 
173
185
  private _value?: string
174
- private _valueIcon?: TT['icons']
175
- private _valueIconColor?: TT['colors'] | null
186
+ private _valueIcon?: IconName
187
+ private _valueIconColor?: ColorName | null
176
188
  private _valueClass?: string[]
177
189
 
178
190
  private _href?: string
@@ -182,7 +194,7 @@ class LabeledValueFragment<TT extends ThemeType> extends ContentFragment<TT> {
182
194
 
183
195
  private _tooltip?: string
184
196
 
185
- value(value: string, icon?: TT['icons'], iconColor?: TT['colors'] | null) {
197
+ value(value: string, icon?: IconName, iconColor?: ColorName | null) {
186
198
  this._value = value
187
199
  this._valueIcon = icon
188
200
  this._valueIconColor = iconColor
@@ -258,10 +270,10 @@ type ListValueDefinition = {
258
270
  tooltip?: string
259
271
  }
260
272
 
261
- class LabeledListFragment<TT extends ThemeType> extends ContentFragment<TT> {
273
+ class LabeledListFragment extends ContentFragment {
262
274
  private _values?: ListValueDefinition[]
263
275
 
264
- constructor(theme: Theme<TT>) {
276
+ constructor(theme: Theme) {
265
277
  super('tt-labeled-list', theme)
266
278
  }
267
279
 
@@ -307,7 +319,7 @@ class LabeledListFragment<TT extends ThemeType> extends ContentFragment<TT> {
307
319
  * Creates a new card fragment builder.
308
320
  * Make sure to call `render()` in order to render it to a parent tag.
309
321
  */
310
- function card<TT extends ThemeType>(theme: Theme<TT>) {
322
+ function card(theme: Theme) {
311
323
  return new CardFragment(theme)
312
324
  }
313
325
 
@@ -316,7 +328,7 @@ function card<TT extends ThemeType>(theme: Theme<TT>) {
316
328
  * Creates a new panel fragment builder.
317
329
  * Make sure to call `render()` in order to render it to a parent tag.
318
330
  */
319
- function panel<TT extends ThemeType>(theme: Theme<TT>) {
331
+ function panel(theme: Theme) {
320
332
  return new PanelFragment(theme)
321
333
  }
322
334
 
@@ -324,18 +336,18 @@ function panel<TT extends ThemeType>(theme: Theme<TT>) {
324
336
  * Creates a new labeled value fragment builder.
325
337
  * Make sure to call `render()` in order to render it to a parent tag.
326
338
  */
327
- function labeledValue<TT extends ThemeType>(theme: Theme<TT>) {
339
+ function labeledValue(theme: Theme) {
328
340
  return new LabeledValueFragment(theme)
329
341
  }
330
342
 
331
- function labeledList<TT extends ThemeType>(theme: Theme<TT>) {
343
+ function labeledList(theme: Theme) {
332
344
  return new LabeledListFragment(theme)
333
345
  }
334
346
 
335
347
  /**
336
348
  * Create a new button in the parent.
337
349
  */
338
- function button<TT extends ThemeType>(parent: PartTag, theme: Theme<TT>, title?: string, icon?: TT['icons'], iconColor: TT['colors'] | null = null) {
350
+ function button(parent: PartTag, theme: Theme, title?: string, icon?: IconName, iconColor: ColorName | null = null) {
339
351
  return parent.a('.tt-button', button => {
340
352
  if (icon) theme.renderIcon(button, icon, iconColor)
341
353
  if (title?.length) button.div('.title', {text: title})
@@ -346,7 +358,7 @@ function button<TT extends ThemeType>(parent: PartTag, theme: Theme<TT>, title?:
346
358
  * Create a new simple value display in the parent.
347
359
  * This is just some text with an optional icon that doesn't have a separate label.
348
360
  */
349
- function simpleValue<TT extends ThemeType>(parent: PartTag, theme: Theme<TT>, title: string, icon?: TT['icons'], iconColor: TT['colors'] | null = 'link') {
361
+ function simpleValue(parent: PartTag, theme: Theme, title: string, icon?: IconName, iconColor: ColorName | null = 'link') {
350
362
  return parent.div('.tt-simple-value.shrink', button => {
351
363
  if (icon) theme.renderIcon(button, icon, iconColor)
352
364
  button.div('.title', {text: title})
@@ -356,7 +368,7 @@ function simpleValue<TT extends ThemeType>(parent: PartTag, theme: Theme<TT>, ti
356
368
  /**
357
369
  * Helper to create a heading with an optional icon.
358
370
  */
359
- function simpleHeading<TT extends ThemeType>(parent: PartTag, theme: Theme<TT>, title: string, icon?: TT['icons']) {
371
+ function simpleHeading(parent: PartTag, theme: Theme, title: string, icon?: IconName) {
360
372
  return parent.h3('.shrink', heading => {
361
373
  if (icon) {
362
374
  theme.renderIcon(heading, icon, 'link')