terrier-engine 4.0.21 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.ts +37 -27
- package/dropdowns.ts +27 -22
- package/forms.ts +33 -0
- package/fragments.ts +39 -37
- package/gen/hub-icons.ts +697 -0
- package/glyps.ts +2 -2
- 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 +9 -22
- package/loading.ts +5 -6
- package/modals.ts +8 -19
- package/overlays.ts +100 -33
- package/package.json +1 -1
- package/parts/content-part.ts +187 -0
- package/parts/not-found-page.ts +20 -0
- package/parts/page-part.ts +189 -0
- package/parts/panel-part.ts +40 -0
- package/parts/terrier-form-part.ts +20 -0
- package/parts/terrier-part.ts +89 -0
- package/schema.ts +28 -1
- package/tabs.ts +164 -0
- package/theme.ts +41 -12
- package/toasts.ts +10 -10
- package/tooltips.ts +2 -2
- package/parts.ts +0 -485
package/overlays.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Part, PartTag, StatelessPart, NoState, PartConstructor} from "tuff-core/parts"
|
|
2
2
|
import { Size, Box, Side } from "tuff-core/box"
|
|
3
3
|
import { Logger } from "tuff-core/logging"
|
|
4
|
+
import {arrays} from "tuff-core";
|
|
4
5
|
|
|
5
6
|
const log = new Logger('Overlays')
|
|
6
7
|
|
|
@@ -8,67 +9,133 @@ const log = new Logger('Overlays')
|
|
|
8
9
|
// Part
|
|
9
10
|
////////////////////////////////////////////////////////////////////////////////
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
+
const OverlayLayerTypes = ['modal', 'dropdown', 'lightbox', 'jump'] as const
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
-
* There can only be one part per layer.
|
|
15
|
+
* The type of overlay for any given layer.
|
|
16
16
|
*/
|
|
17
|
-
export type
|
|
17
|
+
export type OverlayLayerType = typeof OverlayLayerTypes[number]
|
|
18
18
|
|
|
19
19
|
export class OverlayPart extends Part<NoState> {
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
layerStates: OverlayLayerState<any, any>[] = []
|
|
22
|
+
|
|
23
|
+
updateLayers(): StatelessPart[] {
|
|
24
|
+
return this.assignCollection('layers', OverlayLayer, this.layerStates)
|
|
25
|
+
}
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
|
-
* Creates a part
|
|
25
|
-
* Discards the old part at that layer, if there was one.
|
|
28
|
+
* Creates a part and pushes it onto the overlay stack.
|
|
26
29
|
* @param constructor
|
|
27
30
|
* @param state
|
|
28
|
-
* @param
|
|
31
|
+
* @param type
|
|
32
|
+
* @return the new part
|
|
29
33
|
*/
|
|
30
|
-
|
|
31
|
-
constructor:
|
|
34
|
+
pushLayer<PartType extends Part<StateType>, StateType extends {}>(
|
|
35
|
+
constructor: PartConstructor<PartType, StateType>,
|
|
32
36
|
state: StateType,
|
|
33
|
-
|
|
37
|
+
type: OverlayLayerType
|
|
34
38
|
): PartType {
|
|
35
|
-
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
return part
|
|
39
|
+
this.layerStates.push({partClass: constructor, partState: state, type})
|
|
40
|
+
const parts = this.updateLayers()
|
|
41
|
+
return (parts[parts.length-1] as OverlayLayer<PartType, StateType>).part as PartType
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
|
-
*
|
|
43
|
-
* @param
|
|
45
|
+
* Same as `pushLayer`, except that it will re-use the first existing layer of the given type, if present.
|
|
46
|
+
* @param constructor
|
|
47
|
+
* @param state
|
|
48
|
+
* @param type
|
|
49
|
+
* @return the new or existing part
|
|
44
50
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
getOrCreateLayer<PartType extends Part<StateType>, StateType extends {}>(
|
|
52
|
+
constructor: PartConstructor<PartType, StateType>,
|
|
53
|
+
state: StateType,
|
|
54
|
+
type: OverlayLayerType
|
|
55
|
+
): PartType {
|
|
56
|
+
const layers = this.getCollectionParts('layers')
|
|
57
|
+
for (const layer of layers) {
|
|
58
|
+
if (layer.state.type == type) {
|
|
59
|
+
return (layer as OverlayLayer<PartType, StateType>).part as PartType
|
|
60
|
+
}
|
|
50
61
|
}
|
|
51
|
-
this.
|
|
62
|
+
const part = this.pushLayer(constructor, state, type)
|
|
63
|
+
return part as PartType
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
/**
|
|
55
|
-
*
|
|
67
|
+
* Pops the top layer off the overlay stack.
|
|
68
|
+
* @return the part that was popped, if there was one.
|
|
56
69
|
*/
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
popLayer(type?: OverlayLayerType): StatelessPart | undefined {
|
|
71
|
+
const oldParts = this.getCollectionParts('layers')
|
|
72
|
+
if (type) {
|
|
73
|
+
for (let i = this.layerStates.length-1; i>=0; i--) {
|
|
74
|
+
if (this.layerStates[i].type == type) {
|
|
75
|
+
const part = oldParts[i]
|
|
76
|
+
this.layerStates = arrays.without(this.layerStates, this.layerStates[i])
|
|
77
|
+
this.updateLayers()
|
|
78
|
+
return part
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// no type specified, pop the top one
|
|
85
|
+
this.layerStates = this.layerStates.slice(0, this.layerStates.length - 1)
|
|
86
|
+
this.updateLayers()
|
|
87
|
+
return oldParts[oldParts.length - 1]
|
|
60
88
|
}
|
|
61
89
|
}
|
|
62
90
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Removes the layer with the given state from the stack.
|
|
93
|
+
* @param state
|
|
94
|
+
* @return true if there was a layer actually removed
|
|
95
|
+
*/
|
|
96
|
+
removeLayer<StateType>(state: StateType): boolean {
|
|
97
|
+
for (const layerState of this.layerStates) {
|
|
98
|
+
if (layerState.partState == state) {
|
|
99
|
+
this.layerStates = arrays.without(this.layerStates, layerState)
|
|
100
|
+
this.updateLayers()
|
|
101
|
+
return true
|
|
68
102
|
}
|
|
69
103
|
}
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clear all overlay layers.
|
|
109
|
+
*/
|
|
110
|
+
clearAll() {
|
|
111
|
+
this.layerStates = []
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
render(parent: PartTag) {
|
|
115
|
+
this.renderCollection(parent, 'layers')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type OverlayLayerState<PartType extends Part<StateType>, StateType extends {}> = {
|
|
121
|
+
partClass: PartConstructor<PartType, StateType>
|
|
122
|
+
partState: StateType
|
|
123
|
+
type: OverlayLayerType
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
class OverlayLayer<PartType extends Part<StateType>, StateType extends {}> extends Part<OverlayLayerState<PartType, StateType>> {
|
|
127
|
+
|
|
128
|
+
part!: PartType
|
|
129
|
+
|
|
130
|
+
async init() {
|
|
131
|
+
this.part = this.makePart(this.state.partClass, this.state.partState)
|
|
70
132
|
}
|
|
71
133
|
|
|
134
|
+
render(parent: PartTag) {
|
|
135
|
+
parent.part(this.part)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
72
139
|
}
|
|
73
140
|
|
|
74
141
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {Action, IconName} from "../theme"
|
|
2
|
+
import {PartParent, PartTag} from "tuff-core/parts"
|
|
3
|
+
import {Dropdown} from "../dropdowns"
|
|
4
|
+
import TerrierPart from "./terrier-part"
|
|
5
|
+
|
|
6
|
+
export type PanelActions = {
|
|
7
|
+
primary: Array<Action>
|
|
8
|
+
secondary: Array<Action>
|
|
9
|
+
tertiary: Array<Action>
|
|
10
|
+
}
|
|
11
|
+
export type ActionLevel = keyof PanelActions
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Base class for all Parts that render some main content, like pages, panels, and modals.
|
|
15
|
+
*/
|
|
16
|
+
export default abstract class ContentPart<TState> extends TerrierPart<TState> {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* All ContentParts must implement this to render their actual content.
|
|
20
|
+
* @param parent
|
|
21
|
+
*/
|
|
22
|
+
abstract renderContent(parent: PartTag): void
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
render(parent: PartTag) {
|
|
26
|
+
this.renderContent(parent)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
protected _title = ''
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sets the page, panel, or modal title.
|
|
34
|
+
* @param title
|
|
35
|
+
*/
|
|
36
|
+
setTitle(title: string) {
|
|
37
|
+
this._title = title
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected _icon: IconName | null = null
|
|
41
|
+
|
|
42
|
+
setIcon(icon: IconName) {
|
|
43
|
+
this._icon = icon
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected _titleClasses: string[] = []
|
|
47
|
+
|
|
48
|
+
addTitleClass(c: string) {
|
|
49
|
+
this._titleClasses.push(c)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
/// Actions
|
|
54
|
+
|
|
55
|
+
// stored actions can be either an action object or a reference to a named action
|
|
56
|
+
private _actions = {
|
|
57
|
+
primary: Array<Action | string>(),
|
|
58
|
+
secondary: Array<Action | string>(),
|
|
59
|
+
tertiary: Array<Action | string>()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private _namedActions: Record<string, { action: Action, level: ActionLevel }> = {}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Add an action to the part, or replace a named action if it already exists.
|
|
66
|
+
* @param action the action to add
|
|
67
|
+
* @param level whether it's a primary, secondary, or tertiary action
|
|
68
|
+
* @param name a name to be given to this action, so it can be accessed later
|
|
69
|
+
*/
|
|
70
|
+
addAction(action: Action, level: ActionLevel = 'primary', name?: string) {
|
|
71
|
+
if (name?.length) {
|
|
72
|
+
if (name in this._namedActions) {
|
|
73
|
+
const currentLevel = this._namedActions[name].level
|
|
74
|
+
if (level != currentLevel) {
|
|
75
|
+
const index = this._actions[currentLevel].indexOf(name)
|
|
76
|
+
this._actions[currentLevel].splice(index, 1)
|
|
77
|
+
this._actions[level].push(name)
|
|
78
|
+
}
|
|
79
|
+
this._namedActions[name].action = action
|
|
80
|
+
} else {
|
|
81
|
+
this._namedActions[name] = {action, level}
|
|
82
|
+
this._actions[level].push(name)
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
this._actions[level].push(action)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns the action definition for the action with the given name, or undefined if there is no action with that name
|
|
91
|
+
* @param name
|
|
92
|
+
*/
|
|
93
|
+
getNamedAction(name: string): Action | undefined {
|
|
94
|
+
return this._namedActions[name].action
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Removes the action with the given name
|
|
99
|
+
* @param name
|
|
100
|
+
*/
|
|
101
|
+
removeNamedAction(name: string) {
|
|
102
|
+
if (!(name in this._namedActions)) return
|
|
103
|
+
const level = this._actions[this._namedActions[name].level]
|
|
104
|
+
delete this._namedActions[name]
|
|
105
|
+
const actionIndex = level.indexOf(name)
|
|
106
|
+
if (actionIndex >= 0) {
|
|
107
|
+
level.splice(actionIndex, 1)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Clears the actions for this part
|
|
113
|
+
* @param level whether to clear the primary, secondary, or both sets of actions
|
|
114
|
+
*/
|
|
115
|
+
clearActions(level: ActionLevel) {
|
|
116
|
+
for (const action of this._actions[level]) {
|
|
117
|
+
if (typeof action === 'string') {
|
|
118
|
+
delete this._namedActions[action]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
this._actions[level] = []
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
getAllActions(): PanelActions {
|
|
125
|
+
return {
|
|
126
|
+
primary: this.getActions('primary'),
|
|
127
|
+
secondary: this.getActions('secondary'),
|
|
128
|
+
tertiary: this.getActions('tertiary'),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getActions(level: ActionLevel): Action[] {
|
|
133
|
+
return this._actions[level].map(action => {
|
|
134
|
+
return (typeof action === 'string') ? this._namedActions[action].action : action
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
hasActions(level: ActionLevel): boolean {
|
|
139
|
+
return this._actions[level].length > 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
/// Dropdowns
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Shows the given dropdown part on the page.
|
|
147
|
+
* It's generally better to call `toggleDropdown` instead so that the dropdown will be
|
|
148
|
+
* hidden upon a subsequent click on the target.
|
|
149
|
+
* @param constructor a constructor for a dropdown part
|
|
150
|
+
* @param state the dropdown's state
|
|
151
|
+
* @param target the target element around which to show the dropdown
|
|
152
|
+
*/
|
|
153
|
+
makeDropdown<DropdownType extends Dropdown<DropdownStateType>, DropdownStateType extends {}>(
|
|
154
|
+
constructor: { new(p: PartParent, id: string, state: DropdownStateType): DropdownType; },
|
|
155
|
+
state: DropdownStateType,
|
|
156
|
+
target: EventTarget | null) {
|
|
157
|
+
if (!(target && target instanceof HTMLElement)) {
|
|
158
|
+
throw "Trying to show a dropdown without an element target!"
|
|
159
|
+
}
|
|
160
|
+
const dropdown = this.app.addOverlay(constructor, state, 'dropdown')
|
|
161
|
+
dropdown.parentPart = this
|
|
162
|
+
dropdown.anchor(target)
|
|
163
|
+
this.app.lastDropdownTarget = target
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
clearDropdowns() {
|
|
167
|
+
this.app.clearDropdowns()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Calls `makeDropdown` only if there's not a dropdown currently originating from the target.
|
|
172
|
+
* @param constructor a constructor for a dropdown part
|
|
173
|
+
* @param state the dropdown's state
|
|
174
|
+
* @param target the target element around which to show the dropdown
|
|
175
|
+
*/
|
|
176
|
+
toggleDropdown<DropdownType extends Dropdown<DropdownStateType>, DropdownStateType extends {}>(
|
|
177
|
+
constructor: { new(p: PartParent, id: string, state: DropdownStateType): DropdownType; },
|
|
178
|
+
state: DropdownStateType,
|
|
179
|
+
target: EventTarget | null) {
|
|
180
|
+
if (target && target instanceof HTMLElement && target == this.app.lastDropdownTarget) {
|
|
181
|
+
this.clearDropdowns()
|
|
182
|
+
} else {
|
|
183
|
+
this.makeDropdown(constructor, state, target)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {Logger} from "tuff-core/logging"
|
|
2
|
+
import PagePart from "./page-part"
|
|
3
|
+
import {NoState, PartTag} from "tuff-core/parts"
|
|
4
|
+
|
|
5
|
+
const log = new Logger('NotFoundRoute')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default page part if the router can't find the path.
|
|
9
|
+
*/
|
|
10
|
+
export default class NotFoundRoute extends PagePart<NoState> {
|
|
11
|
+
async init() {
|
|
12
|
+
this.setTitle("Page Not Found")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
renderContent(parent: PartTag) {
|
|
16
|
+
log.warn(`Not found: ${this.context.href}`)
|
|
17
|
+
parent.h1({text: "Not Found"})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {Action, RenderActionOptions} from "../theme"
|
|
2
|
+
import ContentPart, {ActionLevel} from "./content-part"
|
|
3
|
+
import {PartTag} from "tuff-core/parts"
|
|
4
|
+
import {optionsForSelect, SelectOptions} from "tuff-core/forms"
|
|
5
|
+
import {UntypedKey} from "tuff-core/messages"
|
|
6
|
+
import {Logger} from "tuff-core/logging"
|
|
7
|
+
import {HtmlParentTag} from "tuff-core/html"
|
|
8
|
+
|
|
9
|
+
const log = new Logger("Terrier PagePart")
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether some content should be constrained to a reasonable width or span the entire screen.
|
|
13
|
+
*/
|
|
14
|
+
export type ContentWidth = "normal" | "wide"
|
|
15
|
+
|
|
16
|
+
/// Toolbar fields
|
|
17
|
+
|
|
18
|
+
type BaseFieldDef = { name: string } & ToolbarFieldDefOptions
|
|
19
|
+
|
|
20
|
+
type ToolbarFieldDefOptions = {
|
|
21
|
+
onChangeKey?: UntypedKey,
|
|
22
|
+
onInputKey?: UntypedKey,
|
|
23
|
+
defaultValue?: string,
|
|
24
|
+
tooltip?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ToolbarSelectDef = { type: 'select', options: SelectOptions } & BaseFieldDef
|
|
28
|
+
|
|
29
|
+
type ValuedInputType = 'text' | 'color' | 'date' | 'datetime-local' | 'email' | 'hidden' | 'month' | 'number' | 'password' | 'search' | 'tel' | 'time' | 'url' | 'week'
|
|
30
|
+
type ToolbarValuedInputDef = { type: ValuedInputType } & BaseFieldDef
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Defines a field to be rendered in the page's toolbar
|
|
34
|
+
*/
|
|
35
|
+
type ToolbarFieldDef = ToolbarSelectDef | ToolbarValuedInputDef
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A part that renders content to a full page.
|
|
39
|
+
*/
|
|
40
|
+
export default abstract class PagePart<TState> extends ContentPart<TState> {
|
|
41
|
+
|
|
42
|
+
/// Content Width
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Whether the main content should be constrained to a reasonable width (default) or span the entire screen.
|
|
46
|
+
*/
|
|
47
|
+
protected mainContentWidth: ContentWidth = "normal"
|
|
48
|
+
|
|
49
|
+
/// Breadcrumbs
|
|
50
|
+
|
|
51
|
+
private _breadcrumbs = Array<Action>()
|
|
52
|
+
|
|
53
|
+
addBreadcrumb(crumb: Action) {
|
|
54
|
+
this._breadcrumbs.push(crumb)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private _titleHref?: string
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Adds an href to the title (last) breadcrumb.
|
|
61
|
+
* @param href
|
|
62
|
+
*/
|
|
63
|
+
setTitleHref(href: string) {
|
|
64
|
+
this._titleHref = href
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Toolbar Fields
|
|
68
|
+
|
|
69
|
+
private _toolbarFieldsOrder: string[] = []
|
|
70
|
+
private _toolbarFields: Record<string, ToolbarFieldDef> = {}
|
|
71
|
+
|
|
72
|
+
protected get hasToolbarFields() {
|
|
73
|
+
return this._toolbarFieldsOrder.length > 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Adds a select to the toolbar with the given options.
|
|
78
|
+
* @param name the name of the select
|
|
79
|
+
* @param selectOptions an array of select options
|
|
80
|
+
* @param opts
|
|
81
|
+
*/
|
|
82
|
+
addToolbarSelect(name: string, selectOptions: SelectOptions, opts?: ToolbarFieldDefOptions) {
|
|
83
|
+
this.addToolbarFieldDef({ type: 'select', name, options: selectOptions, ...opts })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Adds a select to the toolbar with the given options.
|
|
88
|
+
* @param name the name of the select
|
|
89
|
+
* @param type the type attribute of the input field
|
|
90
|
+
* @param opts
|
|
91
|
+
*/
|
|
92
|
+
addToolbarInput(name: string, type: ValuedInputType, opts?: ToolbarFieldDefOptions) {
|
|
93
|
+
this.addToolbarFieldDef({ type, name, ...opts })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private addToolbarFieldDef(def: ToolbarFieldDef) {
|
|
97
|
+
this._toolbarFieldsOrder.push(def.name)
|
|
98
|
+
this._toolbarFields[def.name] = def
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Rendering
|
|
102
|
+
|
|
103
|
+
protected get toolbarClasses() : string[] {
|
|
104
|
+
return []
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
render(parent: PartTag) {
|
|
108
|
+
parent.div(`.tt-page-part.content-width-${this.mainContentWidth}`, page => {
|
|
109
|
+
page.div('.tt-toolbar', toolbar => {
|
|
110
|
+
toolbar.class(...this.toolbarClasses)
|
|
111
|
+
this.renderBreadcrumbs(toolbar)
|
|
112
|
+
this.renderCustomToolbar(toolbar)
|
|
113
|
+
if (this.hasToolbarFields) this.renderToolbarFields(toolbar)
|
|
114
|
+
if (this.hasActions('tertiary')) this.renderActions(toolbar, 'tertiary')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
page.div('.lighting')
|
|
118
|
+
page.div('.full-width-page', conatiner => {
|
|
119
|
+
conatiner.div('.page-content', main => {
|
|
120
|
+
this.renderContent(main)
|
|
121
|
+
main.div('.page-actions', actions => {
|
|
122
|
+
this.renderActions(actions, 'secondary', {iconColor: null, defaultClass: 'secondary'})
|
|
123
|
+
this.renderActions(actions, 'primary', {iconColor: null, defaultClass: 'primary'})
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected renderActions(parent: PartTag, level: ActionLevel, options?: RenderActionOptions) {
|
|
131
|
+
parent.div(`.${level}-actions`, actions => {
|
|
132
|
+
this.app.theme.renderActions(actions, this.getActions(level), options)
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
protected renderBreadcrumbs(parent: PartTag) {
|
|
137
|
+
if (!this._breadcrumbs.length && !this._title?.length) return
|
|
138
|
+
|
|
139
|
+
parent.h1('.breadcrumbs', h1 => {
|
|
140
|
+
const crumbs = Array.from(this._breadcrumbs)
|
|
141
|
+
|
|
142
|
+
// add a breadcrumb for the page title
|
|
143
|
+
if (this._title?.length) {
|
|
144
|
+
const titleCrumb: Action = {
|
|
145
|
+
title: this._title,
|
|
146
|
+
icon: this._icon || undefined,
|
|
147
|
+
}
|
|
148
|
+
if (this._titleHref) {
|
|
149
|
+
titleCrumb.href = this._titleHref
|
|
150
|
+
}
|
|
151
|
+
if (this._titleClasses?.length) {
|
|
152
|
+
titleCrumb.classes = this._titleClasses
|
|
153
|
+
}
|
|
154
|
+
crumbs.push(titleCrumb)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.app.theme.renderActions(h1, crumbs)
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
protected renderCustomToolbar(_parent: PartTag): void {
|
|
162
|
+
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
protected renderToolbarFields(parent: PartTag) {
|
|
166
|
+
parent.div('.fields.tt-flex.align-center.small-gap', fields => {
|
|
167
|
+
for (const name of this._toolbarFieldsOrder) {
|
|
168
|
+
const def = this._toolbarFields[name]
|
|
169
|
+
if (!def) {
|
|
170
|
+
log.warn(`No select def with name ${name} could be found!`)
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let field!: HtmlParentTag
|
|
175
|
+
if (def.type === 'select') {
|
|
176
|
+
field = fields.select({name: def.name}, select => {
|
|
177
|
+
optionsForSelect(select, def.options, def.defaultValue)
|
|
178
|
+
})
|
|
179
|
+
} else {
|
|
180
|
+
field = fields.input({name: def.name, type: def.type})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (def.onChangeKey) field.emitChange(def.onChangeKey)
|
|
184
|
+
if (def.onInputKey) field.emitInput(def.onInputKey)
|
|
185
|
+
if (def.tooltip?.length) field.dataAttr('tooltip', def.tooltip)
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import ContentPart from "./content-part"
|
|
2
|
+
import {PartTag} from "tuff-core/parts"
|
|
3
|
+
import Fragments from "../fragments"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A part that renders content inside a panel.
|
|
7
|
+
*/
|
|
8
|
+
export default abstract class PanelPart<TState> extends ContentPart<TState> {
|
|
9
|
+
|
|
10
|
+
getLoadingContainer() {
|
|
11
|
+
return this.element?.getElementsByClassName('tt-panel')[0]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
protected get panelClasses(): string[] {
|
|
15
|
+
return []
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
render(parent: PartTag) {
|
|
19
|
+
parent.div('.tt-panel', panel => {
|
|
20
|
+
panel.class(...this.panelClasses)
|
|
21
|
+
if (this._title?.length || this.hasActions('tertiary')) {
|
|
22
|
+
panel.div('.panel-header', header => {
|
|
23
|
+
header.h2(h2 => {
|
|
24
|
+
if (this._icon) {
|
|
25
|
+
this.app.theme.renderIcon(h2, this._icon, 'link')
|
|
26
|
+
}
|
|
27
|
+
h2.div('.title', {text: this._title || 'Call setTitle()'})
|
|
28
|
+
})
|
|
29
|
+
header.div('.tertiary-actions', actions => {
|
|
30
|
+
this.theme.renderActions(actions, this.getActions('tertiary'))
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
panel.div('.panel-content', content => {
|
|
35
|
+
this.renderContent(content)
|
|
36
|
+
})
|
|
37
|
+
Fragments.panelActions(panel, this.getAllActions(), this.theme)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {FormPart, FormPartData} from "tuff-core/forms"
|
|
2
|
+
import Theme from "../theme"
|
|
3
|
+
import {TerrierApp} from "../app"
|
|
4
|
+
|
|
5
|
+
export default abstract class TerrierFormPart<TState extends FormPartData> extends FormPart<TState> {
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
get app(): TerrierApp<any> {
|
|
9
|
+
return this.root as unknown as TerrierApp<any> // this should always be true
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get theme(): Theme {
|
|
13
|
+
return this.app.theme
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
get parentClasses(): Array<string> {
|
|
18
|
+
return ['tt-form']
|
|
19
|
+
}
|
|
20
|
+
}
|