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.
Files changed (286) hide show
  1. package/app.ts +37 -27
  2. package/dropdowns.ts +27 -22
  3. package/forms.ts +33 -0
  4. package/fragments.ts +39 -37
  5. package/gen/hub-icons.ts +697 -0
  6. package/glyps.ts +2 -2
  7. package/images/icons/active.svg +1 -0
  8. package/images/icons/admin.svg +1 -0
  9. package/images/icons/archive.svg +1 -0
  10. package/images/icons/arrow_down.svg +1 -0
  11. package/images/icons/arrow_left.svg +1 -0
  12. package/images/icons/arrow_right.svg +1 -0
  13. package/images/icons/arrow_up.svg +1 -0
  14. package/images/icons/assign.svg +1 -0
  15. package/images/icons/attachment.svg +1 -0
  16. package/images/icons/back.svg +1 -0
  17. package/images/icons/badge.svg +1 -0
  18. package/images/icons/board.svg +1 -0
  19. package/images/icons/branch.svg +1 -0
  20. package/images/icons/bug.svg +1 -0
  21. package/images/icons/calculator.svg +1 -0
  22. package/images/icons/checkmark.svg +1 -0
  23. package/images/icons/close.svg +1 -0
  24. package/images/icons/clypboard.svg +1 -0
  25. package/images/icons/comment.svg +1 -0
  26. package/images/icons/complete.svg +1 -0
  27. package/images/icons/dashboard.svg +1 -0
  28. package/images/icons/data_pull.svg +1 -0
  29. package/images/icons/data_update.svg +1 -0
  30. package/images/icons/database.svg +1 -0
  31. package/images/icons/day.svg +1 -0
  32. package/images/icons/delete.svg +1 -0
  33. package/images/icons/documentation.svg +1 -0
  34. package/images/icons/edit.svg +1 -0
  35. package/images/icons/feature.svg +1 -0
  36. package/images/icons/flex.svg +1 -0
  37. package/images/icons/forward.svg +1 -0
  38. package/images/icons/github.svg +1 -0
  39. package/images/icons/history.svg +1 -0
  40. package/images/icons/home.svg +1 -0
  41. package/images/icons/image.svg +1 -0
  42. package/images/icons/inbox.svg +1 -0
  43. package/images/icons/info.svg +1 -0
  44. package/images/icons/issue.svg +1 -0
  45. package/images/icons/lane.svg +1 -0
  46. package/images/icons/lane_asap.svg +1 -0
  47. package/images/icons/lane_days.svg +1 -0
  48. package/images/icons/lane_hours.svg +1 -0
  49. package/images/icons/lane_weeks.svg +1 -0
  50. package/images/icons/lanes_board.svg +1 -0
  51. package/images/icons/level_complete.svg +1 -0
  52. package/images/icons/level_highway.svg +1 -0
  53. package/images/icons/level_on_ramp.svg +1 -0
  54. package/images/icons/level_parking.svg +1 -0
  55. package/images/icons/minus.svg +1 -0
  56. package/images/icons/night.svg +1 -0
  57. package/images/icons/origin.svg +1 -0
  58. package/images/icons/pending.svg +1 -0
  59. package/images/icons/plus.svg +1 -0
  60. package/images/icons/post.svg +1 -0
  61. package/images/icons/pr_closed.svg +1 -0
  62. package/images/icons/pr_merged.svg +1 -0
  63. package/images/icons/pr_open.svg +1 -0
  64. package/images/icons/prioritized.svg +1 -0
  65. package/images/icons/project.svg +1 -0
  66. package/images/icons/question.svg +1 -0
  67. package/images/icons/reaction.svg +1 -0
  68. package/images/icons/recent.svg +1 -0
  69. package/images/icons/refresh.svg +1 -0
  70. package/images/icons/request.svg +1 -0
  71. package/images/icons/settings.svg +1 -0
  72. package/images/icons/status.svg +1 -0
  73. package/images/icons/step_deploy.svg +1 -0
  74. package/images/icons/step_develop.svg +1 -0
  75. package/images/icons/step_investigate.svg +1 -0
  76. package/images/icons/step_review.svg +1 -0
  77. package/images/icons/step_test.svg +1 -0
  78. package/images/icons/steps.svg +1 -0
  79. package/images/icons/steps_board.svg +1 -0
  80. package/images/icons/subscribe.svg +1 -0
  81. package/images/icons/support.svg +1 -0
  82. package/images/icons/terrier.svg +1 -0
  83. package/images/icons/thumbs_up.svg +1 -0
  84. package/images/icons/type.svg +1 -0
  85. package/images/icons/unprioritized.svg +1 -0
  86. package/images/icons/upload.svg +1 -0
  87. package/images/icons/user.svg +1 -0
  88. package/images/icons/users.svg +1 -0
  89. package/images/optimized/icon-active.svg +1 -0
  90. package/images/optimized/icon-admin.svg +1 -0
  91. package/images/optimized/icon-archive.svg +1 -0
  92. package/images/optimized/icon-arrow_down.svg +1 -0
  93. package/images/optimized/icon-arrow_left.svg +1 -0
  94. package/images/optimized/icon-arrow_right.svg +1 -0
  95. package/images/optimized/icon-arrow_up.svg +1 -0
  96. package/images/optimized/icon-assign.svg +1 -0
  97. package/images/optimized/icon-attachment.svg +1 -0
  98. package/images/optimized/icon-back.svg +1 -0
  99. package/images/optimized/icon-badge.svg +1 -0
  100. package/images/optimized/icon-board.svg +1 -0
  101. package/images/optimized/icon-branch.svg +1 -0
  102. package/images/optimized/icon-bug.svg +1 -0
  103. package/images/optimized/icon-calculator.svg +1 -0
  104. package/images/optimized/icon-checkmark.svg +1 -0
  105. package/images/optimized/icon-close.svg +1 -0
  106. package/images/optimized/icon-clypboard.svg +1 -0
  107. package/images/optimized/icon-comment.svg +1 -0
  108. package/images/optimized/icon-complete.svg +1 -0
  109. package/images/optimized/icon-dashboard.svg +1 -0
  110. package/images/optimized/icon-data_pull.svg +1 -0
  111. package/images/optimized/icon-data_update.svg +1 -0
  112. package/images/optimized/icon-database.svg +1 -0
  113. package/images/optimized/icon-day.svg +1 -0
  114. package/images/optimized/icon-delete.svg +1 -0
  115. package/images/optimized/icon-documentation.svg +1 -0
  116. package/images/optimized/icon-edit.svg +1 -0
  117. package/images/optimized/icon-feature.svg +1 -0
  118. package/images/optimized/icon-flex.svg +1 -0
  119. package/images/optimized/icon-forward.svg +1 -0
  120. package/images/optimized/icon-github.svg +1 -0
  121. package/images/optimized/icon-history.svg +1 -0
  122. package/images/optimized/icon-home.svg +1 -0
  123. package/images/optimized/icon-image.svg +1 -0
  124. package/images/optimized/icon-inbox.svg +1 -0
  125. package/images/optimized/icon-info.svg +1 -0
  126. package/images/optimized/icon-issue.svg +1 -0
  127. package/images/optimized/icon-lane.svg +1 -0
  128. package/images/optimized/icon-lane_asap.svg +1 -0
  129. package/images/optimized/icon-lane_days.svg +1 -0
  130. package/images/optimized/icon-lane_hours.svg +1 -0
  131. package/images/optimized/icon-lane_weeks.svg +1 -0
  132. package/images/optimized/icon-lanes_board.svg +1 -0
  133. package/images/optimized/icon-level_complete.svg +1 -0
  134. package/images/optimized/icon-level_highway.svg +1 -0
  135. package/images/optimized/icon-level_on_ramp.svg +1 -0
  136. package/images/optimized/icon-level_parking.svg +1 -0
  137. package/images/optimized/icon-minus.svg +1 -0
  138. package/images/optimized/icon-night.svg +1 -0
  139. package/images/optimized/icon-origin.svg +1 -0
  140. package/images/optimized/icon-pending.svg +1 -0
  141. package/images/optimized/icon-plus.svg +1 -0
  142. package/images/optimized/icon-post.svg +1 -0
  143. package/images/optimized/icon-pr_closed.svg +1 -0
  144. package/images/optimized/icon-pr_merged.svg +1 -0
  145. package/images/optimized/icon-pr_open.svg +1 -0
  146. package/images/optimized/icon-prioritized.svg +1 -0
  147. package/images/optimized/icon-project.svg +1 -0
  148. package/images/optimized/icon-question.svg +1 -0
  149. package/images/optimized/icon-reaction.svg +1 -0
  150. package/images/optimized/icon-recent.svg +1 -0
  151. package/images/optimized/icon-refresh.svg +1 -0
  152. package/images/optimized/icon-request.svg +1 -0
  153. package/images/optimized/icon-settings.svg +1 -0
  154. package/images/optimized/icon-status.svg +1 -0
  155. package/images/optimized/icon-step_deploy.svg +1 -0
  156. package/images/optimized/icon-step_develop.svg +1 -0
  157. package/images/optimized/icon-step_investigate.svg +1 -0
  158. package/images/optimized/icon-step_review.svg +1 -0
  159. package/images/optimized/icon-step_test.svg +1 -0
  160. package/images/optimized/icon-steps.svg +1 -0
  161. package/images/optimized/icon-steps_board.svg +1 -0
  162. package/images/optimized/icon-subscribe.svg +1 -0
  163. package/images/optimized/icon-support.svg +1 -0
  164. package/images/optimized/icon-terrier.svg +1 -0
  165. package/images/optimized/icon-thumbs_up.svg +1 -0
  166. package/images/optimized/icon-type.svg +1 -0
  167. package/images/optimized/icon-unprioritized.svg +1 -0
  168. package/images/optimized/icon-upload.svg +1 -0
  169. package/images/optimized/icon-user.svg +1 -0
  170. package/images/optimized/icon-users.svg +1 -0
  171. package/images/optimized/terrier-hub-favicon.svg +1 -0
  172. package/images/optimized/terrier-hub-icon-dark.svg +1 -0
  173. package/images/optimized/terrier-hub-icon-light.svg +1 -0
  174. package/images/optimized/terrier-hub-loader.svg +1 -0
  175. package/images/optimized/terrier-hub-logo-dark.svg +1 -0
  176. package/images/optimized/terrier-hub-logo-light.svg +1 -0
  177. package/images/raw/icon-active.svg +8 -0
  178. package/images/raw/icon-admin.svg +9 -0
  179. package/images/raw/icon-archive.svg +9 -0
  180. package/images/raw/icon-arrow_down.svg +7 -0
  181. package/images/raw/icon-arrow_left.svg +7 -0
  182. package/images/raw/icon-arrow_right.svg +7 -0
  183. package/images/raw/icon-arrow_up.svg +7 -0
  184. package/images/raw/icon-assign.svg +8 -0
  185. package/images/raw/icon-attachment.svg +7 -0
  186. package/images/raw/icon-back.svg +7 -0
  187. package/images/raw/icon-badge.svg +10 -0
  188. package/images/raw/icon-board.svg +20 -0
  189. package/images/raw/icon-branch.svg +11 -0
  190. package/images/raw/icon-bug.svg +8 -0
  191. package/images/raw/icon-calculator.svg +31 -0
  192. package/images/raw/icon-checkmark.svg +8 -0
  193. package/images/raw/icon-close.svg +8 -0
  194. package/images/raw/icon-clypboard.svg +9 -0
  195. package/images/raw/icon-comment.svg +12 -0
  196. package/images/raw/icon-complete.svg +8 -0
  197. package/images/raw/icon-dashboard.svg +18 -0
  198. package/images/raw/icon-data_pull.svg +9 -0
  199. package/images/raw/icon-data_update.svg +9 -0
  200. package/images/raw/icon-database.svg +10 -0
  201. package/images/raw/icon-day.svg +19 -0
  202. package/images/raw/icon-delete.svg +11 -0
  203. package/images/raw/icon-documentation.svg +21 -0
  204. package/images/raw/icon-edit.svg +11 -0
  205. package/images/raw/icon-feature.svg +7 -0
  206. package/images/raw/icon-flex.svg +6 -0
  207. package/images/raw/icon-forward.svg +7 -0
  208. package/images/raw/icon-github.svg +8 -0
  209. package/images/raw/icon-history.svg +12 -0
  210. package/images/raw/icon-home.svg +8 -0
  211. package/images/raw/icon-image.svg +9 -0
  212. package/images/raw/icon-inbox.svg +9 -0
  213. package/images/raw/icon-info.svg +11 -0
  214. package/images/raw/icon-issue.svg +10 -0
  215. package/images/raw/icon-lane.svg +9 -0
  216. package/images/raw/icon-lane_asap.svg +9 -0
  217. package/images/raw/icon-lane_days.svg +11 -0
  218. package/images/raw/icon-lane_hours.svg +8 -0
  219. package/images/raw/icon-lane_weeks.svg +10 -0
  220. package/images/raw/icon-lanes_board.svg +10 -0
  221. package/images/raw/icon-level_complete.svg +8 -0
  222. package/images/raw/icon-level_highway.svg +11 -0
  223. package/images/raw/icon-level_on_ramp.svg +8 -0
  224. package/images/raw/icon-level_parking.svg +10 -0
  225. package/images/raw/icon-minus.svg +8 -0
  226. package/images/raw/icon-night.svg +9 -0
  227. package/images/raw/icon-origin.svg +11 -0
  228. package/images/raw/icon-pending.svg +10 -0
  229. package/images/raw/icon-plus.svg +8 -0
  230. package/images/raw/icon-post.svg +12 -0
  231. package/images/raw/icon-pr_closed.svg +15 -0
  232. package/images/raw/icon-pr_merged.svg +11 -0
  233. package/images/raw/icon-pr_open.svg +12 -0
  234. package/images/raw/icon-prioritized.svg +11 -0
  235. package/images/raw/icon-project.svg +24 -0
  236. package/images/raw/icon-question.svg +9 -0
  237. package/images/raw/icon-reaction.svg +6 -0
  238. package/images/raw/icon-recent.svg +12 -0
  239. package/images/raw/icon-refresh.svg +11 -0
  240. package/images/raw/icon-request.svg +10 -0
  241. package/images/raw/icon-settings.svg +11 -0
  242. package/images/raw/icon-status.svg +8 -0
  243. package/images/raw/icon-step_deploy.svg +15 -0
  244. package/images/raw/icon-step_develop.svg +12 -0
  245. package/images/raw/icon-step_investigate.svg +14 -0
  246. package/images/raw/icon-step_review.svg +11 -0
  247. package/images/raw/icon-step_test.svg +14 -0
  248. package/images/raw/icon-steps.svg +18 -0
  249. package/images/raw/icon-steps_board.svg +19 -0
  250. package/images/raw/icon-subscribe.svg +10 -0
  251. package/images/raw/icon-support.svg +14 -0
  252. package/images/raw/icon-terrier.svg +7 -0
  253. package/images/raw/icon-thumbs_up.svg +1 -0
  254. package/images/raw/icon-type.svg +15 -0
  255. package/images/raw/icon-unprioritized.svg +10 -0
  256. package/images/raw/icon-upload.svg +8 -0
  257. package/images/raw/icon-user.svg +9 -0
  258. package/images/raw/icon-users.svg +14 -0
  259. package/images/raw/terrier-hub-favicon-alert.png +0 -0
  260. package/images/raw/terrier-hub-favicon-dark.png +0 -0
  261. package/images/raw/terrier-hub-favicon.png +0 -0
  262. package/images/raw/terrier-hub-favicon.svg +29 -0
  263. package/images/raw/terrier-hub-icon-dark.svg +23 -0
  264. package/images/raw/terrier-hub-icon-light.png +0 -0
  265. package/images/raw/terrier-hub-icon-light.svg +23 -0
  266. package/images/raw/terrier-hub-loader.svg +54 -0
  267. package/images/raw/terrier-hub-logo-dark.svg +27 -0
  268. package/images/raw/terrier-hub-logo-light.png +0 -0
  269. package/images/raw/terrier-hub-logo-light.svg +28 -0
  270. package/lightbox.ts +9 -22
  271. package/loading.ts +5 -6
  272. package/modals.ts +8 -19
  273. package/overlays.ts +100 -33
  274. package/package.json +1 -1
  275. package/parts/content-part.ts +187 -0
  276. package/parts/not-found-page.ts +20 -0
  277. package/parts/page-part.ts +189 -0
  278. package/parts/panel-part.ts +40 -0
  279. package/parts/terrier-form-part.ts +20 -0
  280. package/parts/terrier-part.ts +89 -0
  281. package/schema.ts +28 -1
  282. package/tabs.ts +164 -0
  283. package/theme.ts +41 -12
  284. package/toasts.ts +10 -10
  285. package/tooltips.ts +2 -2
  286. package/parts.ts +0 -485
package/overlays.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { Part, PartParent, PartTag, StatelessPart, NoState } from "tuff-core/parts"
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 OverlayLayers = ['modal', 'dropdown', 'lightbox', 'jump'] as const
12
+ const OverlayLayerTypes = ['modal', 'dropdown', 'lightbox', 'jump'] as const
12
13
 
13
14
  /**
14
- * Which overlay layer to use for an overlay part.
15
- * There can only be one part per layer.
15
+ * The type of overlay for any given layer.
16
16
  */
17
- export type OverlayLayer = typeof OverlayLayers[number]
17
+ export type OverlayLayerType = typeof OverlayLayerTypes[number]
18
18
 
19
19
  export class OverlayPart extends Part<NoState> {
20
20
 
21
- parts: {[layer in OverlayLayer]?: StatelessPart} = {}
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 at the given layer.
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 layer
31
+ * @param type
32
+ * @return the new part
29
33
  */
30
- makeLayer<PartType extends Part<StateType>, StateType>(
31
- constructor: { new(p: PartParent, id: string, state: StateType): PartType; },
34
+ pushLayer<PartType extends Part<StateType>, StateType extends {}>(
35
+ constructor: PartConstructor<PartType, StateType>,
32
36
  state: StateType,
33
- layer: OverlayLayer
37
+ type: OverlayLayerType
34
38
  ): PartType {
35
- const part = this.makePart(constructor, state)
36
- this.clearLayer(layer)
37
- this.parts[layer] = part
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
- * Clear a single overlay layer.
43
- * @param layer
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
- clearLayer(layer: OverlayLayer) {
46
- const layerPart = this.parts[layer]
47
- if (layerPart) {
48
- this.removeChild(layerPart)
49
- this.parts[layer] = undefined
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.dirty()
62
+ const part = this.pushLayer(constructor, state, type)
63
+ return part as PartType
52
64
  }
53
65
 
54
66
  /**
55
- * Clear all overlay layers.
67
+ * Pops the top layer off the overlay stack.
68
+ * @return the part that was popped, if there was one.
56
69
  */
57
- clearAll() {
58
- for (const layer of OverlayLayers) {
59
- this.clearLayer(layer)
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
- render(parent: PartTag) {
64
- for (const layer of OverlayLayers) {
65
- const part = this.parts[layer]
66
- if (part) {
67
- parent.div(layer).part(part)
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
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "*"
6
6
  ],
7
- "version": "4.0.21",
7
+ "version": "4.3.0",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/Terrier-Tech/terrier-engine"
@@ -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
+ }