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.
- package/api.ts +120 -1
- package/app.ts +65 -13
- package/attachments.ts +55 -0
- package/dropdowns.ts +6 -17
- package/format.ts +24 -0
- package/forms.ts +52 -1
- package/fragments.ts +42 -30
- package/gen/hub-icons.ts +697 -0
- package/glyps.ts +2 -2
- package/ids.ts +12 -0
- package/images/icons/active.svg +1 -0
- package/images/icons/admin.svg +1 -0
- package/images/icons/archive.svg +1 -0
- package/images/icons/arrow_down.svg +1 -0
- package/images/icons/arrow_left.svg +1 -0
- package/images/icons/arrow_right.svg +1 -0
- package/images/icons/arrow_up.svg +1 -0
- package/images/icons/assign.svg +1 -0
- package/images/icons/attachment.svg +1 -0
- package/images/icons/back.svg +1 -0
- package/images/icons/badge.svg +1 -0
- package/images/icons/board.svg +1 -0
- package/images/icons/branch.svg +1 -0
- package/images/icons/bug.svg +1 -0
- package/images/icons/calculator.svg +1 -0
- package/images/icons/checkmark.svg +1 -0
- package/images/icons/close.svg +1 -0
- package/images/icons/clypboard.svg +1 -0
- package/images/icons/comment.svg +1 -0
- package/images/icons/complete.svg +1 -0
- package/images/icons/dashboard.svg +1 -0
- package/images/icons/data_pull.svg +1 -0
- package/images/icons/data_update.svg +1 -0
- package/images/icons/database.svg +1 -0
- package/images/icons/day.svg +1 -0
- package/images/icons/delete.svg +1 -0
- package/images/icons/documentation.svg +1 -0
- package/images/icons/edit.svg +1 -0
- package/images/icons/feature.svg +1 -0
- package/images/icons/flex.svg +1 -0
- package/images/icons/forward.svg +1 -0
- package/images/icons/github.svg +1 -0
- package/images/icons/history.svg +1 -0
- package/images/icons/home.svg +1 -0
- package/images/icons/image.svg +1 -0
- package/images/icons/inbox.svg +1 -0
- package/images/icons/info.svg +1 -0
- package/images/icons/issue.svg +1 -0
- package/images/icons/lane.svg +1 -0
- package/images/icons/lane_asap.svg +1 -0
- package/images/icons/lane_days.svg +1 -0
- package/images/icons/lane_hours.svg +1 -0
- package/images/icons/lane_weeks.svg +1 -0
- package/images/icons/lanes_board.svg +1 -0
- package/images/icons/level_complete.svg +1 -0
- package/images/icons/level_highway.svg +1 -0
- package/images/icons/level_on_ramp.svg +1 -0
- package/images/icons/level_parking.svg +1 -0
- package/images/icons/minus.svg +1 -0
- package/images/icons/night.svg +1 -0
- package/images/icons/origin.svg +1 -0
- package/images/icons/pending.svg +1 -0
- package/images/icons/plus.svg +1 -0
- package/images/icons/post.svg +1 -0
- package/images/icons/pr_closed.svg +1 -0
- package/images/icons/pr_merged.svg +1 -0
- package/images/icons/pr_open.svg +1 -0
- package/images/icons/prioritized.svg +1 -0
- package/images/icons/project.svg +1 -0
- package/images/icons/question.svg +1 -0
- package/images/icons/reaction.svg +1 -0
- package/images/icons/recent.svg +1 -0
- package/images/icons/refresh.svg +1 -0
- package/images/icons/request.svg +1 -0
- package/images/icons/settings.svg +1 -0
- package/images/icons/status.svg +1 -0
- package/images/icons/step_deploy.svg +1 -0
- package/images/icons/step_develop.svg +1 -0
- package/images/icons/step_investigate.svg +1 -0
- package/images/icons/step_review.svg +1 -0
- package/images/icons/step_test.svg +1 -0
- package/images/icons/steps.svg +1 -0
- package/images/icons/steps_board.svg +1 -0
- package/images/icons/subscribe.svg +1 -0
- package/images/icons/support.svg +1 -0
- package/images/icons/terrier.svg +1 -0
- package/images/icons/thumbs_up.svg +1 -0
- package/images/icons/type.svg +1 -0
- package/images/icons/unprioritized.svg +1 -0
- package/images/icons/upload.svg +1 -0
- package/images/icons/user.svg +1 -0
- package/images/icons/users.svg +1 -0
- package/images/optimized/icon-active.svg +1 -0
- package/images/optimized/icon-admin.svg +1 -0
- package/images/optimized/icon-archive.svg +1 -0
- package/images/optimized/icon-arrow_down.svg +1 -0
- package/images/optimized/icon-arrow_left.svg +1 -0
- package/images/optimized/icon-arrow_right.svg +1 -0
- package/images/optimized/icon-arrow_up.svg +1 -0
- package/images/optimized/icon-assign.svg +1 -0
- package/images/optimized/icon-attachment.svg +1 -0
- package/images/optimized/icon-back.svg +1 -0
- package/images/optimized/icon-badge.svg +1 -0
- package/images/optimized/icon-board.svg +1 -0
- package/images/optimized/icon-branch.svg +1 -0
- package/images/optimized/icon-bug.svg +1 -0
- package/images/optimized/icon-calculator.svg +1 -0
- package/images/optimized/icon-checkmark.svg +1 -0
- package/images/optimized/icon-close.svg +1 -0
- package/images/optimized/icon-clypboard.svg +1 -0
- package/images/optimized/icon-comment.svg +1 -0
- package/images/optimized/icon-complete.svg +1 -0
- package/images/optimized/icon-dashboard.svg +1 -0
- package/images/optimized/icon-data_pull.svg +1 -0
- package/images/optimized/icon-data_update.svg +1 -0
- package/images/optimized/icon-database.svg +1 -0
- package/images/optimized/icon-day.svg +1 -0
- package/images/optimized/icon-delete.svg +1 -0
- package/images/optimized/icon-documentation.svg +1 -0
- package/images/optimized/icon-edit.svg +1 -0
- package/images/optimized/icon-feature.svg +1 -0
- package/images/optimized/icon-flex.svg +1 -0
- package/images/optimized/icon-forward.svg +1 -0
- package/images/optimized/icon-github.svg +1 -0
- package/images/optimized/icon-history.svg +1 -0
- package/images/optimized/icon-home.svg +1 -0
- package/images/optimized/icon-image.svg +1 -0
- package/images/optimized/icon-inbox.svg +1 -0
- package/images/optimized/icon-info.svg +1 -0
- package/images/optimized/icon-issue.svg +1 -0
- package/images/optimized/icon-lane.svg +1 -0
- package/images/optimized/icon-lane_asap.svg +1 -0
- package/images/optimized/icon-lane_days.svg +1 -0
- package/images/optimized/icon-lane_hours.svg +1 -0
- package/images/optimized/icon-lane_weeks.svg +1 -0
- package/images/optimized/icon-lanes_board.svg +1 -0
- package/images/optimized/icon-level_complete.svg +1 -0
- package/images/optimized/icon-level_highway.svg +1 -0
- package/images/optimized/icon-level_on_ramp.svg +1 -0
- package/images/optimized/icon-level_parking.svg +1 -0
- package/images/optimized/icon-minus.svg +1 -0
- package/images/optimized/icon-night.svg +1 -0
- package/images/optimized/icon-origin.svg +1 -0
- package/images/optimized/icon-pending.svg +1 -0
- package/images/optimized/icon-plus.svg +1 -0
- package/images/optimized/icon-post.svg +1 -0
- package/images/optimized/icon-pr_closed.svg +1 -0
- package/images/optimized/icon-pr_merged.svg +1 -0
- package/images/optimized/icon-pr_open.svg +1 -0
- package/images/optimized/icon-prioritized.svg +1 -0
- package/images/optimized/icon-project.svg +1 -0
- package/images/optimized/icon-question.svg +1 -0
- package/images/optimized/icon-reaction.svg +1 -0
- package/images/optimized/icon-recent.svg +1 -0
- package/images/optimized/icon-refresh.svg +1 -0
- package/images/optimized/icon-request.svg +1 -0
- package/images/optimized/icon-settings.svg +1 -0
- package/images/optimized/icon-status.svg +1 -0
- package/images/optimized/icon-step_deploy.svg +1 -0
- package/images/optimized/icon-step_develop.svg +1 -0
- package/images/optimized/icon-step_investigate.svg +1 -0
- package/images/optimized/icon-step_review.svg +1 -0
- package/images/optimized/icon-step_test.svg +1 -0
- package/images/optimized/icon-steps.svg +1 -0
- package/images/optimized/icon-steps_board.svg +1 -0
- package/images/optimized/icon-subscribe.svg +1 -0
- package/images/optimized/icon-support.svg +1 -0
- package/images/optimized/icon-terrier.svg +1 -0
- package/images/optimized/icon-thumbs_up.svg +1 -0
- package/images/optimized/icon-type.svg +1 -0
- package/images/optimized/icon-unprioritized.svg +1 -0
- package/images/optimized/icon-upload.svg +1 -0
- package/images/optimized/icon-user.svg +1 -0
- package/images/optimized/icon-users.svg +1 -0
- package/images/optimized/terrier-hub-favicon.svg +1 -0
- package/images/optimized/terrier-hub-icon-dark.svg +1 -0
- package/images/optimized/terrier-hub-icon-light.svg +1 -0
- package/images/optimized/terrier-hub-loader.svg +1 -0
- package/images/optimized/terrier-hub-logo-dark.svg +1 -0
- package/images/optimized/terrier-hub-logo-light.svg +1 -0
- package/images/raw/icon-active.svg +8 -0
- package/images/raw/icon-admin.svg +9 -0
- package/images/raw/icon-archive.svg +9 -0
- package/images/raw/icon-arrow_down.svg +7 -0
- package/images/raw/icon-arrow_left.svg +7 -0
- package/images/raw/icon-arrow_right.svg +7 -0
- package/images/raw/icon-arrow_up.svg +7 -0
- package/images/raw/icon-assign.svg +8 -0
- package/images/raw/icon-attachment.svg +7 -0
- package/images/raw/icon-back.svg +7 -0
- package/images/raw/icon-badge.svg +10 -0
- package/images/raw/icon-board.svg +20 -0
- package/images/raw/icon-branch.svg +11 -0
- package/images/raw/icon-bug.svg +8 -0
- package/images/raw/icon-calculator.svg +31 -0
- package/images/raw/icon-checkmark.svg +8 -0
- package/images/raw/icon-close.svg +8 -0
- package/images/raw/icon-clypboard.svg +9 -0
- package/images/raw/icon-comment.svg +12 -0
- package/images/raw/icon-complete.svg +8 -0
- package/images/raw/icon-dashboard.svg +18 -0
- package/images/raw/icon-data_pull.svg +9 -0
- package/images/raw/icon-data_update.svg +9 -0
- package/images/raw/icon-database.svg +10 -0
- package/images/raw/icon-day.svg +19 -0
- package/images/raw/icon-delete.svg +11 -0
- package/images/raw/icon-documentation.svg +21 -0
- package/images/raw/icon-edit.svg +11 -0
- package/images/raw/icon-feature.svg +7 -0
- package/images/raw/icon-flex.svg +6 -0
- package/images/raw/icon-forward.svg +7 -0
- package/images/raw/icon-github.svg +8 -0
- package/images/raw/icon-history.svg +12 -0
- package/images/raw/icon-home.svg +8 -0
- package/images/raw/icon-image.svg +9 -0
- package/images/raw/icon-inbox.svg +9 -0
- package/images/raw/icon-info.svg +11 -0
- package/images/raw/icon-issue.svg +10 -0
- package/images/raw/icon-lane.svg +9 -0
- package/images/raw/icon-lane_asap.svg +9 -0
- package/images/raw/icon-lane_days.svg +11 -0
- package/images/raw/icon-lane_hours.svg +8 -0
- package/images/raw/icon-lane_weeks.svg +10 -0
- package/images/raw/icon-lanes_board.svg +10 -0
- package/images/raw/icon-level_complete.svg +8 -0
- package/images/raw/icon-level_highway.svg +11 -0
- package/images/raw/icon-level_on_ramp.svg +8 -0
- package/images/raw/icon-level_parking.svg +10 -0
- package/images/raw/icon-minus.svg +8 -0
- package/images/raw/icon-night.svg +9 -0
- package/images/raw/icon-origin.svg +11 -0
- package/images/raw/icon-pending.svg +10 -0
- package/images/raw/icon-plus.svg +8 -0
- package/images/raw/icon-post.svg +12 -0
- package/images/raw/icon-pr_closed.svg +15 -0
- package/images/raw/icon-pr_merged.svg +11 -0
- package/images/raw/icon-pr_open.svg +12 -0
- package/images/raw/icon-prioritized.svg +11 -0
- package/images/raw/icon-project.svg +24 -0
- package/images/raw/icon-question.svg +9 -0
- package/images/raw/icon-reaction.svg +6 -0
- package/images/raw/icon-recent.svg +12 -0
- package/images/raw/icon-refresh.svg +11 -0
- package/images/raw/icon-request.svg +10 -0
- package/images/raw/icon-settings.svg +11 -0
- package/images/raw/icon-status.svg +8 -0
- package/images/raw/icon-step_deploy.svg +15 -0
- package/images/raw/icon-step_develop.svg +12 -0
- package/images/raw/icon-step_investigate.svg +14 -0
- package/images/raw/icon-step_review.svg +11 -0
- package/images/raw/icon-step_test.svg +14 -0
- package/images/raw/icon-steps.svg +18 -0
- package/images/raw/icon-steps_board.svg +19 -0
- package/images/raw/icon-subscribe.svg +10 -0
- package/images/raw/icon-support.svg +14 -0
- package/images/raw/icon-terrier.svg +7 -0
- package/images/raw/icon-thumbs_up.svg +1 -0
- package/images/raw/icon-type.svg +15 -0
- package/images/raw/icon-unprioritized.svg +10 -0
- package/images/raw/icon-upload.svg +8 -0
- package/images/raw/icon-user.svg +9 -0
- package/images/raw/icon-users.svg +14 -0
- package/images/raw/terrier-hub-favicon-alert.png +0 -0
- package/images/raw/terrier-hub-favicon-dark.png +0 -0
- package/images/raw/terrier-hub-favicon.png +0 -0
- package/images/raw/terrier-hub-favicon.svg +29 -0
- package/images/raw/terrier-hub-icon-dark.svg +23 -0
- package/images/raw/terrier-hub-icon-light.png +0 -0
- package/images/raw/terrier-hub-icon-light.svg +23 -0
- package/images/raw/terrier-hub-loader.svg +54 -0
- package/images/raw/terrier-hub-logo-dark.svg +27 -0
- package/images/raw/terrier-hub-logo-light.png +0 -0
- package/images/raw/terrier-hub-logo-light.svg +28 -0
- package/lightbox.ts +8 -21
- package/loading.ts +5 -6
- package/modals.ts +41 -19
- package/overlays.ts +2 -2
- package/package.json +4 -2
- package/parts/content-part.ts +24 -25
- package/parts/not-found-page.ts +1 -7
- package/parts/page-part.ts +47 -54
- package/parts/panel-part.ts +1 -8
- package/parts/terrier-form-part.ts +20 -0
- package/parts/terrier-part.ts +51 -13
- package/schema.ts +29 -1
- package/sheets.ts +100 -0
- package/tabs.ts +190 -0
- package/theme.ts +41 -12
- package/toasts.ts +9 -9
- package/tooltips.ts +2 -2
- 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
|
|
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!:
|
|
26
|
-
|
|
27
|
-
get theme():
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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,
|
|
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
|
|
10
|
-
protected constructor(readonly prefix: string, readonly theme: Theme
|
|
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?:
|
|
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?:
|
|
38
|
+
protected _icon?: IconName
|
|
27
39
|
|
|
28
40
|
/**
|
|
29
41
|
* @param i the panel icon
|
|
30
42
|
*/
|
|
31
|
-
icon(i:
|
|
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
|
|
50
|
-
constructor(theme: Theme
|
|
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
|
|
58
|
-
secondary: Array<Action
|
|
59
|
-
tertiary: Array<Action
|
|
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
|
|
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
|
|
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
|
|
126
|
-
constructor(theme: Theme
|
|
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
|
|
179
|
+
class LabeledValueFragment extends ContentFragment {
|
|
168
180
|
|
|
169
|
-
constructor(theme: Theme
|
|
181
|
+
constructor(theme: Theme) {
|
|
170
182
|
super('tt-labeled-value', theme)
|
|
171
183
|
}
|
|
172
184
|
|
|
173
185
|
private _value?: string
|
|
174
|
-
private _valueIcon?:
|
|
175
|
-
private _valueIconColor?:
|
|
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?:
|
|
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
|
|
273
|
+
class LabeledListFragment extends ContentFragment {
|
|
262
274
|
private _values?: ListValueDefinition[]
|
|
263
275
|
|
|
264
|
-
constructor(theme: Theme
|
|
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
|
|
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
|
|
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
|
|
339
|
+
function labeledValue(theme: Theme) {
|
|
328
340
|
return new LabeledValueFragment(theme)
|
|
329
341
|
}
|
|
330
342
|
|
|
331
|
-
function labeledList
|
|
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
|
|
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
|
|
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
|
|
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')
|