@xh/hoist 87.1.0 → 87.2.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 (264) hide show
  1. package/.npmignore +39 -0
  2. package/CHANGELOG.md +99 -20
  3. package/admin/AdminUtils.ts +64 -0
  4. package/admin/App.scss +88 -0
  5. package/admin/columns/UserData.ts +30 -3
  6. package/admin/detail/RestDetailModel.ts +64 -0
  7. package/admin/detail/RestDetailPanel.ts +106 -0
  8. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +27 -94
  9. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +23 -60
  10. package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +1 -2
  11. package/admin/tabs/general/alertBanner/AlertBannerPanel.ts +2 -13
  12. package/admin/tabs/general/config/ConfigDetailPanel.ts +72 -0
  13. package/admin/tabs/general/config/ConfigPanel.ts +41 -53
  14. package/admin/tabs/general/config/ConfigPanelModel.ts +3 -0
  15. package/admin/tabs/general/config/ConfigValue.scss +14 -0
  16. package/admin/tabs/general/config/ConfigValue.ts +104 -65
  17. package/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.ts +93 -0
  18. package/admin/tabs/userData/jsonblob/JsonBlobModel.ts +4 -0
  19. package/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +2 -0
  20. package/admin/tabs/userData/prefs/UserPreferenceDetailPanel.ts +62 -0
  21. package/admin/tabs/userData/prefs/UserPreferenceModel.ts +10 -3
  22. package/admin/tabs/userData/prefs/UserPreferencePanel.ts +3 -1
  23. package/admin/tabs/userData/roles/details/RoleDetails.scss +2 -19
  24. package/admin/tabs/userData/roles/details/RoleDetails.ts +13 -13
  25. package/appcontainer/AboutDialogModel.ts +1 -0
  26. package/appcontainer/AppContainerModel.ts +3 -0
  27. package/appcontainer/AppStateModel.ts +1 -0
  28. package/appcontainer/BannerSourceModel.ts +1 -0
  29. package/appcontainer/ChangelogDialogModel.ts +1 -0
  30. package/appcontainer/ColChooserOptionsModel.ts +1 -0
  31. package/appcontainer/ExceptionDialogModel.ts +1 -0
  32. package/appcontainer/FeedbackDialogModel.ts +1 -0
  33. package/appcontainer/ImpersonationBarModel.ts +1 -0
  34. package/appcontainer/MessageSourceModel.ts +1 -0
  35. package/appcontainer/OptionsDialogModel.ts +1 -0
  36. package/appcontainer/PageStateModel.ts +1 -0
  37. package/appcontainer/RouterModel.ts +2 -0
  38. package/appcontainer/SizingModeModel.ts +1 -0
  39. package/appcontainer/ThemeModel.ts +1 -0
  40. package/appcontainer/ToastSourceModel.ts +1 -0
  41. package/appcontainer/UserAgentModel.ts +1 -0
  42. package/appcontainer/ViewportSizeModel.ts +1 -0
  43. package/build/types/admin/AdminUtils.d.ts +29 -0
  44. package/build/types/admin/columns/UserData.d.ts +3 -0
  45. package/build/types/admin/detail/RestDetailModel.d.ts +20 -0
  46. package/build/types/admin/detail/RestDetailPanel.d.ts +34 -0
  47. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +5 -17
  48. package/build/types/admin/tabs/general/config/ConfigDetailPanel.d.ts +7 -0
  49. package/build/types/admin/tabs/general/config/ConfigValue.d.ts +10 -4
  50. package/build/types/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.d.ts +3 -0
  51. package/build/types/admin/tabs/userData/prefs/UserPreferenceDetailPanel.d.ts +3 -0
  52. package/build/types/admin/tabs/userData/prefs/UserPreferenceModel.d.ts +3 -0
  53. package/build/types/appcontainer/AboutDialogModel.d.ts +1 -0
  54. package/build/types/appcontainer/AppContainerModel.d.ts +1 -0
  55. package/build/types/appcontainer/AppStateModel.d.ts +1 -0
  56. package/build/types/appcontainer/BannerSourceModel.d.ts +1 -0
  57. package/build/types/appcontainer/ChangelogDialogModel.d.ts +1 -0
  58. package/build/types/appcontainer/ColChooserOptionsModel.d.ts +1 -0
  59. package/build/types/appcontainer/ExceptionDialogModel.d.ts +1 -0
  60. package/build/types/appcontainer/FeedbackDialogModel.d.ts +1 -0
  61. package/build/types/appcontainer/ImpersonationBarModel.d.ts +1 -0
  62. package/build/types/appcontainer/MessageSourceModel.d.ts +1 -0
  63. package/build/types/appcontainer/OptionsDialogModel.d.ts +1 -0
  64. package/build/types/appcontainer/PageStateModel.d.ts +1 -0
  65. package/build/types/appcontainer/RouterModel.d.ts +1 -0
  66. package/build/types/appcontainer/SizingModeModel.d.ts +1 -0
  67. package/build/types/appcontainer/ThemeModel.d.ts +1 -0
  68. package/build/types/appcontainer/ToastSourceModel.d.ts +1 -0
  69. package/build/types/appcontainer/UserAgentModel.d.ts +1 -0
  70. package/build/types/appcontainer/ViewportSizeModel.d.ts +1 -0
  71. package/build/types/cmp/ag-grid/AgGridModel.d.ts +3 -1
  72. package/build/types/cmp/card/Card.d.ts +2 -2
  73. package/build/types/cmp/chart/ChartModel.d.ts +2 -0
  74. package/build/types/cmp/dataview/DataViewModel.d.ts +2 -0
  75. package/build/types/cmp/daterange/DateRangePickerModel.d.ts +266 -0
  76. package/build/types/cmp/daterange/DateRangePresets.d.ts +26 -0
  77. package/build/types/cmp/daterange/DateRangeUtils.d.ts +96 -0
  78. package/build/types/cmp/daterange/Types.d.ts +152 -0
  79. package/build/types/cmp/daterange/index.d.ts +4 -0
  80. package/build/types/cmp/filter/FilterChooserModel.d.ts +3 -1
  81. package/build/types/cmp/form/FormModel.d.ts +3 -1
  82. package/build/types/cmp/grid/GridModel.d.ts +2 -0
  83. package/build/types/cmp/grid/Types.d.ts +2 -0
  84. package/build/types/cmp/grid/filter/GridFilterModel.d.ts +1 -1
  85. package/build/types/cmp/grouping/GroupingChooserModel.d.ts +3 -1
  86. package/build/types/cmp/input/HoistInputProps.d.ts +49 -2
  87. package/build/types/cmp/tab/TabContainerModel.d.ts +3 -1
  88. package/build/types/cmp/tab/TabModel.d.ts +3 -1
  89. package/build/types/cmp/treemap/TreeMapModel.d.ts +2 -0
  90. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +2 -0
  91. package/build/types/cmp/zoneGrid/ZoneGridModel.d.ts +2 -0
  92. package/build/types/core/HoistBase.d.ts +7 -0
  93. package/build/types/core/HoistProps.d.ts +26 -1
  94. package/build/types/core/model/RootRefreshContextModel.d.ts +1 -0
  95. package/build/types/core/types/Telemetry.d.ts +1 -1
  96. package/build/types/data/Store.d.ts +3 -1
  97. package/build/types/data/StoreSelectionModel.d.ts +3 -1
  98. package/build/types/data/cube/Cube.d.ts +5 -2
  99. package/build/types/data/cube/View.d.ts +2 -0
  100. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -0
  101. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -0
  102. package/build/types/desktop/cmp/button/Button.d.ts +2 -2
  103. package/build/types/desktop/cmp/button/ButtonGroup.d.ts +2 -2
  104. package/build/types/desktop/cmp/dash/DashConfig.d.ts +2 -0
  105. package/build/types/desktop/cmp/dash/DashViewModel.d.ts +3 -1
  106. package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +1 -1
  107. package/build/types/desktop/cmp/dash/container/DashContainerModel.d.ts +1 -1
  108. package/build/types/desktop/cmp/daterange/DateRangePicker.d.ts +66 -0
  109. package/build/types/desktop/cmp/daterange/impl/CustomTab.d.ts +3 -0
  110. package/build/types/desktop/cmp/daterange/impl/DateRangePickerLocalModel.d.ts +121 -0
  111. package/build/types/desktop/cmp/daterange/impl/PeriodTab.d.ts +2 -0
  112. package/build/types/desktop/cmp/daterange/impl/PresetsTab.d.ts +3 -0
  113. package/build/types/desktop/cmp/daterange/impl/RelativeTab.d.ts +3 -0
  114. package/build/types/desktop/cmp/daterange/impl/TabUtils.d.ts +21 -0
  115. package/build/types/desktop/cmp/daterange/index.d.ts +2 -0
  116. package/build/types/desktop/cmp/dock/DockContainerModel.d.ts +3 -1
  117. package/build/types/desktop/cmp/dock/DockViewModel.d.ts +3 -1
  118. package/build/types/desktop/cmp/grouping/GroupingChooser.d.ts +4 -1
  119. package/build/types/desktop/cmp/input/IntentInput.d.ts +38 -0
  120. package/build/types/desktop/cmp/input/NumberInput.d.ts +2 -2
  121. package/build/types/desktop/cmp/input/SegmentedControl.d.ts +25 -4
  122. package/build/types/desktop/cmp/input/TextArea.d.ts +2 -2
  123. package/build/types/desktop/cmp/input/TextInput.d.ts +2 -2
  124. package/build/types/desktop/cmp/input/index.d.ts +1 -0
  125. package/build/types/desktop/cmp/leftrightchooser/LeftRightChooserModel.d.ts +3 -1
  126. package/build/types/desktop/cmp/panel/PanelModel.d.ts +3 -1
  127. package/build/types/desktop/cmp/rest/RestGridModel.d.ts +1 -1
  128. package/build/types/desktop/cmp/tab/impl/TabContainer.d.ts +1 -1
  129. package/build/types/icon/Icon.d.ts +3 -0
  130. package/build/types/inspector/impl/InspectorUtils.d.ts +3 -0
  131. package/build/types/inspector/instances/InstancesModel.d.ts +4 -0
  132. package/build/types/kit/react-day-picker/index.d.ts +3 -3
  133. package/build/types/mobile/cmp/button/Button.d.ts +2 -2
  134. package/build/types/mobile/cmp/input/NumberInput.d.ts +2 -2
  135. package/build/types/mobile/cmp/input/SegmentedControl.d.ts +18 -4
  136. package/build/types/mobile/cmp/input/TextArea.d.ts +2 -2
  137. package/build/types/mobile/cmp/input/TextInput.d.ts +2 -2
  138. package/build/types/mobile/cmp/navigator/NavigatorModel.d.ts +3 -1
  139. package/build/types/mobile/cmp/navigator/PageModel.d.ts +3 -1
  140. package/build/types/mobile/cmp/tab/impl/TabContainer.d.ts +1 -1
  141. package/build/types/svc/InspectorService.d.ts +1 -0
  142. package/build/types/utils/js/LangUtils.d.ts +24 -5
  143. package/cmp/README.md +1 -0
  144. package/cmp/ag-grid/AgGridModel.ts +5 -0
  145. package/cmp/badge/Badge.ts +5 -3
  146. package/cmp/card/Card.ts +4 -3
  147. package/cmp/chart/ChartModel.ts +11 -1
  148. package/cmp/dataview/DataViewModel.ts +7 -1
  149. package/cmp/daterange/DateRangePickerModel.ts +628 -0
  150. package/cmp/daterange/DateRangePresets.ts +274 -0
  151. package/cmp/daterange/DateRangeUtils.ts +557 -0
  152. package/cmp/daterange/README.md +434 -0
  153. package/cmp/daterange/Types.ts +203 -0
  154. package/cmp/daterange/index.ts +10 -0
  155. package/cmp/filter/FilterChooserModel.ts +7 -4
  156. package/cmp/form/FormModel.ts +6 -0
  157. package/cmp/form/README.md +1 -0
  158. package/cmp/grid/GridModel.ts +24 -4
  159. package/cmp/grid/Types.ts +3 -0
  160. package/cmp/grid/filter/GridFilterModel.ts +3 -1
  161. package/cmp/grid/impl/RecordSortUtils.ts +10 -2
  162. package/cmp/grouping/GroupingChooserModel.ts +6 -1
  163. package/cmp/input/HoistInputProps.ts +50 -2
  164. package/cmp/input/README.md +42 -0
  165. package/cmp/layout/Box.ts +3 -2
  166. package/cmp/tab/TabContainerModel.ts +9 -1
  167. package/cmp/tab/TabModel.ts +5 -0
  168. package/cmp/treemap/SplitTreeMapModel.ts +12 -2
  169. package/cmp/treemap/TreeMapModel.ts +6 -1
  170. package/cmp/viewmanager/ViewManagerModel.ts +6 -1
  171. package/cmp/zoneGrid/ZoneGridModel.ts +7 -0
  172. package/core/HoistBase.ts +11 -0
  173. package/core/HoistProps.ts +27 -0
  174. package/core/README.md +46 -0
  175. package/core/impl/InstallServices.ts +5 -1
  176. package/core/model/RootRefreshContextModel.ts +2 -0
  177. package/core/types/Telemetry.ts +1 -1
  178. package/data/README.md +3 -1
  179. package/data/Store.ts +7 -0
  180. package/data/StoreSelectionModel.ts +5 -1
  181. package/data/cube/Cube.ts +12 -3
  182. package/data/cube/View.ts +5 -1
  183. package/desktop/README.md +2 -0
  184. package/desktop/appcontainer/LoginPanel.ts +2 -0
  185. package/desktop/cmp/button/Button.scss +71 -42
  186. package/desktop/cmp/button/Button.ts +10 -1
  187. package/desktop/cmp/button/ButtonGroup.ts +4 -1
  188. package/desktop/cmp/button/grid/ColChooserButton.ts +9 -1
  189. package/desktop/cmp/button/zoneGrid/ZoneMapperButton.ts +1 -0
  190. package/desktop/cmp/dash/DashConfig.ts +3 -0
  191. package/desktop/cmp/dash/DashViewModel.ts +14 -1
  192. package/desktop/cmp/dash/canvas/DashCanvasModel.ts +3 -1
  193. package/desktop/cmp/dash/container/DashContainerModel.ts +3 -1
  194. package/desktop/cmp/daterange/DateRangePicker.scss +577 -0
  195. package/desktop/cmp/daterange/DateRangePicker.ts +357 -0
  196. package/desktop/cmp/daterange/impl/CustomTab.ts +165 -0
  197. package/desktop/cmp/daterange/impl/DateRangePickerLocalModel.ts +482 -0
  198. package/desktop/cmp/daterange/impl/PeriodTab.ts +138 -0
  199. package/desktop/cmp/daterange/impl/PresetsTab.ts +58 -0
  200. package/desktop/cmp/daterange/impl/RelativeTab.ts +108 -0
  201. package/desktop/cmp/daterange/impl/TabUtils.ts +34 -0
  202. package/desktop/cmp/daterange/index.ts +8 -0
  203. package/desktop/cmp/dock/DockContainerModel.ts +6 -1
  204. package/desktop/cmp/dock/DockViewModel.ts +6 -1
  205. package/desktop/cmp/filechooser/FileChooser.ts +4 -0
  206. package/desktop/cmp/form/FormField.ts +1 -0
  207. package/desktop/cmp/grouping/GroupingChooser.ts +8 -2
  208. package/desktop/cmp/input/Checkbox.ts +1 -0
  209. package/desktop/cmp/input/CodeInput.ts +1 -0
  210. package/desktop/cmp/input/DateInput.scss +1 -1
  211. package/desktop/cmp/input/DateInput.ts +1 -0
  212. package/desktop/cmp/input/IntentInput.scss +202 -0
  213. package/desktop/cmp/input/IntentInput.ts +213 -0
  214. package/desktop/cmp/input/NumberInput.ts +11 -2
  215. package/desktop/cmp/input/Picker.ts +1 -0
  216. package/desktop/cmp/input/RadioInput.ts +2 -1
  217. package/desktop/cmp/input/SegmentedControl.scss +45 -2
  218. package/desktop/cmp/input/SegmentedControl.ts +66 -11
  219. package/desktop/cmp/input/Select.ts +9 -7
  220. package/desktop/cmp/input/Slider.ts +2 -0
  221. package/desktop/cmp/input/SwitchInput.ts +1 -0
  222. package/desktop/cmp/input/TextArea.ts +11 -2
  223. package/desktop/cmp/input/TextInput.ts +11 -2
  224. package/desktop/cmp/input/index.ts +1 -0
  225. package/desktop/cmp/leftrightchooser/LeftRightChooserModel.ts +5 -0
  226. package/desktop/cmp/panel/Panel.ts +8 -5
  227. package/desktop/cmp/panel/PanelModel.ts +8 -0
  228. package/desktop/cmp/panel/impl/ResizeContainer.ts +2 -1
  229. package/desktop/cmp/rest/RestGridModel.ts +4 -0
  230. package/desktop/cmp/tab/impl/TabContainer.ts +2 -0
  231. package/docs/README.md +2 -0
  232. package/docs/build-and-publish.md +23 -7
  233. package/docs/doc-registry.json +8 -0
  234. package/docs/version-compatibility.md +2 -0
  235. package/icon/Icon.ts +9 -0
  236. package/icon/index.ts +24 -0
  237. package/inspector/README.md +8 -2
  238. package/inspector/impl/InspectorUtils.ts +12 -0
  239. package/inspector/instances/DiagnosticsModel.ts +3 -2
  240. package/inspector/instances/InstancesModel.ts +81 -14
  241. package/inspector/instances/InstancesPanel.ts +6 -0
  242. package/kit/react-day-picker/index.ts +3 -3
  243. package/mobile/appcontainer/LoginPanel.ts +2 -0
  244. package/mobile/cmp/button/Button.ts +16 -3
  245. package/mobile/cmp/form/FormField.ts +1 -0
  246. package/mobile/cmp/input/Checkbox.ts +1 -0
  247. package/mobile/cmp/input/DateInput.ts +1 -0
  248. package/mobile/cmp/input/Label.ts +13 -8
  249. package/mobile/cmp/input/NumberInput.ts +11 -2
  250. package/mobile/cmp/input/SearchInput.ts +1 -0
  251. package/mobile/cmp/input/SegmentedControl.scss +36 -6
  252. package/mobile/cmp/input/SegmentedControl.ts +37 -11
  253. package/mobile/cmp/input/Select.ts +2 -0
  254. package/mobile/cmp/input/SwitchInput.ts +1 -0
  255. package/mobile/cmp/input/TextArea.ts +11 -2
  256. package/mobile/cmp/input/TextInput.ts +11 -2
  257. package/mobile/cmp/navigator/NavigatorModel.ts +6 -1
  258. package/mobile/cmp/navigator/PageModel.ts +6 -1
  259. package/mobile/cmp/tab/impl/TabContainer.ts +9 -1
  260. package/package.json +19 -19
  261. package/styles/vars.scss +32 -2
  262. package/svc/InspectorService.ts +2 -0
  263. package/svc/TraceService.ts +5 -2
  264. package/utils/js/LangUtils.ts +37 -5
@@ -7,7 +7,8 @@
7
7
  import {FormModel} from '@xh/hoist/cmp/form';
8
8
  import {placeholder, span} from '@xh/hoist/cmp/layout';
9
9
  import {tabContainer, TabContainerModel, TabConfig} from '@xh/hoist/cmp/tab';
10
- import {creates, hoistCmp, HoistModel, managed} from '@xh/hoist/core';
10
+ import {readonlyJsonInput} from '@xh/hoist/admin/AdminUtils';
11
+ import {creates, hoistCmp, HoistModel, managed, PlainObject, XH} from '@xh/hoist/core';
11
12
  import {formField} from '@xh/hoist/desktop/cmp/form';
12
13
  import {
13
14
  CodeInputLineStyles,
@@ -17,7 +18,7 @@ import {
17
18
  textInput
18
19
  } from '@xh/hoist/desktop/cmp/input';
19
20
  import {Icon} from '@xh/hoist/icon';
20
- import {makeObservable} from '@xh/hoist/mobx';
21
+ import {makeObservable, observable} from '@xh/hoist/mobx';
21
22
  import classNames from 'classnames';
22
23
  import {isPlainObject, last, union} from 'lodash';
23
24
  import {ReactElement} from 'react';
@@ -25,9 +26,13 @@ import {buildResolvedJson, changedKeysFromStored} from './ConfigUtils';
25
26
  import './ConfigValue.scss';
26
27
 
27
28
  /**
28
- * Presentation of a config's value within the config editor, bound to the enclosing RestForm's
29
- * `value` field. Plain configs render a single editor by type; configs with a typedClass and/or
30
- * an active instance-config override render a tab set over the applicable views of the value.
29
+ * Presentation of a config's value, bound to the `value` field of an enclosing Form. Plain configs
30
+ * render a single editor by type; configs with a typedClass and/or an active instance-config
31
+ * override render a tab set over the applicable views of the value.
32
+ *
33
+ * Used within the RestGrid editor dialog (editable, opening on the Database tab) and the docked
34
+ * detail panel (read-only, opening on the most-derived view). Pass `height` for a fixed-height
35
+ * display, or omit it to fill the available space.
31
36
  */
32
37
  export const configValue = hoistCmp.factory<ConfigValueModel>({
33
38
  displayName: 'ConfigValue',
@@ -35,22 +40,28 @@ export const configValue = hoistCmp.factory<ConfigValueModel>({
35
40
  model: creates(() => new ConfigValueModel()),
36
41
 
37
42
  render({model, className}) {
43
+ const {height} = model;
44
+ className = classNames(className, height == null ? 'xh-config-value--fill' : null);
38
45
  return model.usesTabs
39
46
  ? tabContainer({model: model.tabContainerModel, className})
40
- : valueFormField(model.valueType, model.height, className);
47
+ : valueFormField(model.valueType, height, className);
41
48
  }
42
49
  });
43
50
 
44
51
  class ConfigValueModel extends HoistModel {
45
52
  override xhImpl = true;
46
53
 
47
- @managed tabContainerModel: TabContainerModel;
54
+ @managed @observable.ref tabContainerModel: TabContainerModel;
48
55
 
49
56
  get formModel(): FormModel {
50
57
  return this.componentProps.formModel;
51
58
  }
52
- get height(): number {
53
- return this.componentProps.height ?? 250;
59
+ /** Fixed height for the value display, or null to fill available space. */
60
+ get height(): number | null {
61
+ return this.componentProps.height ?? null;
62
+ }
63
+ get readonly(): boolean {
64
+ return this.formModel?.readonly ?? false;
54
65
  }
55
66
  get valueField() {
56
67
  return this.formModel?.fields?.value;
@@ -85,16 +96,77 @@ class ConfigValueModel extends HoistModel {
85
96
  }
86
97
 
87
98
  override onLinked() {
88
- if (this.usesTabs) this.buildTabs();
99
+ // Rebuild the tab set whenever the bound data changes - a new record selected into a
100
+ // shared FormModel (detail panel) or the same one re-saved. Tracks the value field's
101
+ // initialValue, not its live value, so typing in the editor does not tear down the tabs.
102
+ this.addReaction({
103
+ track: () => [
104
+ this.resolvedValue,
105
+ this.defaultValue,
106
+ this.overrideValue,
107
+ this.valueType,
108
+ this.valueField?.initialValue
109
+ ],
110
+ run: () => this.buildTabs(),
111
+ fireImmediately: true
112
+ });
89
113
  }
90
114
 
91
115
  private buildTabs() {
92
- const {resolvedValue, defaultValue, overrideValue, valueType, valueField, height} = this,
116
+ XH.safeDestroy(this.tabContainerModel);
117
+ this.tabContainerModel = null;
118
+ if (!this.usesTabs) return;
119
+
120
+ const {
121
+ resolvedValue,
122
+ defaultValue,
123
+ overrideValue,
124
+ valueType,
125
+ valueField,
126
+ height,
127
+ readonly
128
+ } = this,
93
129
  hasOverride = overrideValue != null,
94
130
  hasResolved = resolvedValue != null,
95
131
  hasDefaults = defaultValue != null,
96
132
  tabs: TabConfig[] = [];
97
133
 
134
+ // Tabs run least- to most-derived: Defaults, Database, Instance, Resolved.
135
+ if (hasDefaults) {
136
+ tabs.push({
137
+ id: 'defaults',
138
+ title: 'Defaults',
139
+ icon: Icon.code(),
140
+ content: () =>
141
+ jsonInput({
142
+ value: JSON.stringify(defaultValue),
143
+ readonly: true,
144
+ autoFormat: true,
145
+ enableSearch: true,
146
+ className: 'xh-config-value__defaults',
147
+ ...sizeProps(height)
148
+ })
149
+ });
150
+ }
151
+
152
+ tabs.push({
153
+ id: 'db',
154
+ title: hasOverride
155
+ ? span({className: 'xh-config-value__overridden', item: 'Database'})
156
+ : 'Database',
157
+ icon: Icon.edit(),
158
+ content: () => valueFormField(valueType, height)
159
+ });
160
+
161
+ if (hasOverride) {
162
+ tabs.push({
163
+ id: 'instance',
164
+ title: 'Instance',
165
+ icon: Icon.warning({intent: 'warning'}),
166
+ content: () => readonlyValue(valueType, overrideValue, height)
167
+ });
168
+ }
169
+
98
170
  // Resolved - effective value with typedClass defaults applied.
99
171
  if (hasResolved) {
100
172
  // Highlight keys explicitly set in the database value or an instance override - all
@@ -111,50 +183,15 @@ class ConfigValueModel extends HoistModel {
111
183
  id: 'resolved',
112
184
  title: 'Resolved',
113
185
  icon: Icon.bolt(),
114
- content: () => resolvedTab({text, lineStyles, height})
186
+ content: () => resolvedTab({text, lineStyles, ...sizeProps(height)})
115
187
  });
116
188
  }
117
189
 
118
- if (hasOverride) {
119
- tabs.push({
120
- id: 'instance',
121
- title: 'Instance',
122
- icon: Icon.warning({intent: 'warning'}),
123
- content: () => readonlyValue(valueType, overrideValue, height)
124
- });
125
- }
126
-
127
- tabs.push({
128
- id: 'db',
129
- title: hasOverride
130
- ? span({className: 'xh-config-value__overridden', item: 'Database'})
131
- : 'Database',
132
- icon: Icon.edit(),
133
- content: () => valueFormField(valueType, height)
190
+ // Open on Database if editable, else the most-derived view.
191
+ this.tabContainerModel = new TabContainerModel({
192
+ defaultTabId: readonly ? last(tabs).id : 'db',
193
+ tabs
134
194
  });
135
-
136
- // Defaults - the typedClass defaults as declared in code.
137
- if (hasDefaults) {
138
- tabs.push({
139
- id: 'defaults',
140
- title: 'Defaults',
141
- icon: Icon.code(),
142
- content: () =>
143
- jsonInput({
144
- value: JSON.stringify(defaultValue),
145
- readonly: true,
146
- autoFormat: true,
147
- enableSearch: true,
148
- className: 'xh-config-value__defaults',
149
- height
150
- })
151
- });
152
- }
153
-
154
- // Built most- to least-derived above, but displayed the other way round - opening on the
155
- // last tab, i.e. the most derived view available.
156
- tabs.reverse();
157
- this.tabContainerModel = new TabContainerModel({defaultTabId: last(tabs).id, tabs});
158
195
  }
159
196
  }
160
197
 
@@ -182,7 +219,11 @@ const resolvedTab = hoistCmp.factory<ConfigValueModel>(({model, text, lineStyles
182
219
 
183
220
  // Label-less FormField for `value`, bound via the enclosing Form context for standard validation
184
221
  // display and read-only rendering. The plain branch passes `className` for the full-width rule.
185
- function valueFormField(valueType: string, height: number, className?: string): ReactElement {
222
+ function valueFormField(
223
+ valueType: string,
224
+ height: number | null,
225
+ className?: string
226
+ ): ReactElement {
186
227
  return formField({
187
228
  field: 'value',
188
229
  label: null,
@@ -193,13 +234,13 @@ function valueFormField(valueType: string, height: number, className?: string):
193
234
  }
194
235
 
195
236
  // Editable input for a config value, by type. Bound by the enclosing FormField.
196
- function valueInput(valueType: string, height: number): ReactElement {
237
+ function valueInput(valueType: string, height: number | null): ReactElement {
197
238
  switch (valueType) {
198
239
  case 'json':
199
240
  return jsonInput({
200
241
  autoFormat: true,
201
242
  enableSearch: true,
202
- height
243
+ ...sizeProps(height)
203
244
  });
204
245
  case 'bool':
205
246
  return select({options: [true, false], enableClear: false});
@@ -217,24 +258,22 @@ function valueInput(valueType: string, height: number): ReactElement {
217
258
  }
218
259
 
219
260
  // Read-only display of a raw stored value, by type. Masks pwd.
220
- function readonlyValue(valueType: string, value: any, height: number): ReactElement {
261
+ function readonlyValue(valueType: string, value: any, height: number | null): ReactElement {
221
262
  switch (valueType) {
222
263
  case 'json':
223
- return jsonInput({
224
- value,
225
- readonly: true,
226
- autoFormat: true,
227
- enableSearch: true,
228
- width: null,
229
- height
230
- });
264
+ return readonlyJsonInput(value, height);
231
265
  case 'pwd':
232
- return textInput({value: value == null ? '' : '*****', disabled: true});
266
+ return span(value == null ? '' : '*****');
233
267
  default:
234
- return textInput({value: value?.toString() ?? '', disabled: true});
268
+ return span(value?.toString() ?? '');
235
269
  }
236
270
  }
237
271
 
272
+ // Layout props for a JSON editor - fixed to `height` if given, otherwise filling its container.
273
+ function sizeProps(height: number | null): PlainObject {
274
+ return height != null ? {height} : {flex: 1, height: '100%', width: '100%'};
275
+ }
276
+
238
277
  // Muting styles for pre-rendered JSON `text` - every line NOT in `highlightLines`.
239
278
  function mutedLineStyles(text: string, highlightLines: number[]): CodeInputLineStyles[] {
240
279
  const changed = new Set(highlightLines),
@@ -0,0 +1,93 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2026 Extremely Heavy Industries Inc.
6
+ */
7
+ import {jsonRenderer, yesNoRenderer} from '@xh/hoist/admin/AdminUtils';
8
+ import {badgeRenderer} from '@xh/hoist/admin/columns';
9
+ import {restDetailPanel} from '@xh/hoist/admin/detail/RestDetailPanel';
10
+ import {formFieldSet} from '@xh/hoist/cmp/form';
11
+ import {hbox} from '@xh/hoist/cmp/layout';
12
+ import {hoistCmp, uses} from '@xh/hoist/core';
13
+ import {formField} from '@xh/hoist/desktop/cmp/form';
14
+ import {Icon} from '@xh/hoist/icon';
15
+ import {archivedDateRenderer} from './JsonBlobColumns';
16
+ import {JsonBlobModel} from './JsonBlobModel';
17
+
18
+ /** Read-only detail view of the JSON Blob selected in the grid. */
19
+ export const jsonBlobDetailPanel = hoistCmp.factory<JsonBlobModel>({
20
+ displayName: 'JsonBlobDetailPanel',
21
+ model: uses(JsonBlobModel),
22
+
23
+ render({model}) {
24
+ return restDetailPanel({
25
+ icon: Icon.json(),
26
+ emptyText: 'Select a blob to view its details.',
27
+ persistWith: {...model.persistWith, path: 'detailPanel'},
28
+ renderForm: (record, formModel) => {
29
+ const {description, meta, archived} = formModel.values,
30
+ // Meta is stored as JSON text - a serialized null is as empty as no value.
31
+ hasMeta = meta != null && meta !== 'null';
32
+ return [
33
+ formFieldSet({
34
+ title: 'Blob',
35
+ items: [
36
+ hbox(
37
+ formField({field: 'type', flex: 1}),
38
+ formField({field: 'owner', flex: 1}),
39
+ formField({field: 'acl', flex: 1})
40
+ ),
41
+ formField({field: 'description', omit: !description}),
42
+ hbox(
43
+ formField({
44
+ field: 'token',
45
+ readonlyRenderer: badgeRenderer,
46
+ flex: 1
47
+ }),
48
+ formField({
49
+ field: 'archived',
50
+ readonlyRenderer: yesNoRenderer,
51
+ flex: 1
52
+ }),
53
+ formField({
54
+ field: 'archivedDate',
55
+ readonlyRenderer: archivedDateRenderer,
56
+ omit: !archived,
57
+ flex: 1
58
+ })
59
+ ),
60
+ hbox(
61
+ formField({field: 'dateCreated', label: 'Created', flex: 1}),
62
+ formField({field: 'lastUpdated', label: 'Updated', flex: 1}),
63
+ formField({field: 'lastUpdatedBy', label: 'Updated By', flex: 1})
64
+ )
65
+ ]
66
+ }),
67
+ // Value and Meta share the remaining height 3:2, favoring the value.
68
+ formFieldSet({
69
+ title: 'Value',
70
+ className: 'xh-admin-readonly-form__fill',
71
+ flexGrow: 3,
72
+ item: formField({
73
+ field: 'value',
74
+ label: null,
75
+ readonlyRenderer: jsonRenderer
76
+ })
77
+ }),
78
+ formFieldSet({
79
+ title: 'Meta',
80
+ className: 'xh-admin-readonly-form__fill',
81
+ flexGrow: 2,
82
+ omit: !hasMeta,
83
+ item: formField({
84
+ field: 'meta',
85
+ label: null,
86
+ readonlyRenderer: jsonRenderer
87
+ })
88
+ })
89
+ ];
90
+ }
91
+ });
92
+ }
93
+ });
@@ -68,6 +68,10 @@ export class JsonBlobModel extends HoistModel {
68
68
  {...(Col.lastUpdatedBy.field as FieldSpec), editable: false}
69
69
  ]
70
70
  },
71
+ onRowDoubleClicked: ({data}) => {
72
+ if (data && !this.gridModel.readonly) this.gridModel.editRecord(data);
73
+ },
74
+ toolbarActions: [addAction, editAction, cloneAction, deleteAction],
71
75
  menuActions: [addAction, editAction, cloneAction, deleteAction],
72
76
  prepareCloneFn: ({clone}) => (clone.name = `${clone.name}_CLONE`),
73
77
  sortBy: ['owner', 'name'],
@@ -16,6 +16,7 @@ import {restGrid} from '@xh/hoist/desktop/cmp/rest';
16
16
  import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
17
17
  import {Icon} from '@xh/hoist/icon';
18
18
  import {differ} from '../../../differ/Differ';
19
+ import {jsonBlobDetailPanel} from './JsonBlobDetailPanel';
19
20
  import {JsonBlobModel} from './JsonBlobModel';
20
21
 
21
22
  export const jsonBlobPanel = hoistCmp.factory({
@@ -64,6 +65,7 @@ export const jsonBlobPanel = hoistCmp.factory({
64
65
  ]
65
66
  })
66
67
  }),
68
+ jsonBlobDetailPanel(),
67
69
  differ({omit: !model.differModel})
68
70
  );
69
71
  }
@@ -0,0 +1,62 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2026 Extremely Heavy Industries Inc.
6
+ */
7
+ import {jsonRenderer, rawValueRenderer} from '@xh/hoist/admin/AdminUtils';
8
+ import {valueTypeRenderer} from '@xh/hoist/admin/columns';
9
+ import {restDetailPanel} from '@xh/hoist/admin/detail/RestDetailPanel';
10
+ import {formFieldSet} from '@xh/hoist/cmp/form';
11
+ import {hbox} from '@xh/hoist/cmp/layout';
12
+ import {hoistCmp, uses} from '@xh/hoist/core';
13
+ import {formField} from '@xh/hoist/desktop/cmp/form';
14
+ import {Icon} from '@xh/hoist/icon';
15
+ import {UserPreferenceModel} from './UserPreferenceModel';
16
+
17
+ /** Read-only detail view of the user preference value selected in the grid. */
18
+ export const userPreferenceDetailPanel = hoistCmp.factory<UserPreferenceModel>({
19
+ displayName: 'UserPreferenceDetailPanel',
20
+ model: uses(UserPreferenceModel),
21
+
22
+ render({model}) {
23
+ return restDetailPanel({
24
+ icon: Icon.bookmark(),
25
+ emptyText: 'Select a user preference to view its details.',
26
+ persistWith: {...model.persistWith, path: 'detailPanel'},
27
+ renderForm: (record, formModel) => {
28
+ const {type} = formModel.values,
29
+ isJson = type === 'json';
30
+ return [
31
+ formFieldSet({
32
+ title: 'Preference',
33
+ items: [
34
+ hbox(
35
+ formField({field: 'username', flex: 1}),
36
+ formField({field: 'groupName', flex: 1}),
37
+ formField({
38
+ field: 'type',
39
+ readonlyRenderer: valueTypeRenderer,
40
+ flex: 1
41
+ })
42
+ ),
43
+ hbox(
44
+ formField({field: 'lastUpdatedBy', label: 'Updated By', flex: 1}),
45
+ formField({field: 'lastUpdated', label: 'Updated', flex: 2})
46
+ )
47
+ ]
48
+ }),
49
+ formFieldSet({
50
+ title: 'Value',
51
+ className: isJson ? 'xh-admin-readonly-form__fill' : null,
52
+ item: formField({
53
+ field: 'userValue',
54
+ label: null,
55
+ readonlyRenderer: isJson ? jsonRenderer : rawValueRenderer
56
+ })
57
+ })
58
+ ];
59
+ }
60
+ });
61
+ }
62
+ });
@@ -9,10 +9,12 @@ import {AppModel} from '@xh/hoist/admin/AppModel';
9
9
  import * as Col from '@xh/hoist/admin/columns';
10
10
  import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
11
11
  import {FieldSpec} from '@xh/hoist/data';
12
- import {RestGridModel} from '@xh/hoist/desktop/cmp/rest';
12
+ import {addAction, deleteAction, editAction, RestGridModel} from '@xh/hoist/desktop/cmp/rest';
13
13
  import {bindable, makeObservable} from '@xh/hoist/mobx';
14
14
 
15
15
  export class UserPreferenceModel extends HoistModel {
16
+ override persistWith = {localStorageKey: 'xhAdminUserPreferenceState'};
17
+
16
18
  @managed gridModel: RestGridModel;
17
19
 
18
20
  @bindable showEditorDialog: boolean = false;
@@ -32,11 +34,16 @@ export class UserPreferenceModel extends HoistModel {
32
34
  exportOptions: {filename: exportFilenameWithDate('user-prefs')},
33
35
  filterFields: ['name', 'username'],
34
36
  groupBy: 'groupName',
35
- persistWith: {localStorageKey: 'xhAdminUserPreferenceState'},
37
+ persistWith: this.persistWith,
36
38
  readonly: AppModel.readonly,
37
39
  selModel: 'multiple',
38
40
  sortBy: 'name',
39
41
  unit: 'user preference',
42
+ onRowDoubleClicked: ({data}) => {
43
+ if (data && !this.gridModel.readonly) this.gridModel.editRecord(data);
44
+ },
45
+ toolbarActions: [addAction, editAction, deleteAction],
46
+ menuActions: [addAction, editAction, deleteAction],
40
47
  // Store + fields
41
48
  store: {
42
49
  url: 'rest/userPreferenceAdmin',
@@ -65,7 +72,7 @@ export class UserPreferenceModel extends HoistModel {
65
72
  // Cols + editors
66
73
  columns: [
67
74
  {...Col.name},
68
- {...Col.type},
75
+ {...Col.type, renderer: Col.valueTypeRenderer, align: 'center'},
69
76
  {...Col.username},
70
77
  {...Col.groupName, hidden},
71
78
  {...Col.userValue},
@@ -8,6 +8,7 @@
8
8
  import * as Col from '@xh/hoist/admin/columns/Rest';
9
9
  import * as AdminCol from '@xh/hoist/admin/columns';
10
10
  import {prefEditorDialog} from '@xh/hoist/admin/tabs/userData/prefs/editor/PrefEditorDialog';
11
+ import {userPreferenceDetailPanel} from '@xh/hoist/admin/tabs/userData/prefs/UserPreferenceDetailPanel';
11
12
  import {UserPreferenceModel} from '@xh/hoist/admin/tabs/userData/prefs/UserPreferenceModel';
12
13
  import {hframe} from '@xh/hoist/cmp/layout';
13
14
  import {creates, hoistCmp} from '@xh/hoist/core';
@@ -59,7 +60,8 @@ export const userPreferencePanel = hoistCmp.factory({
59
60
  prefEditorDialog()
60
61
  ],
61
62
  mask: 'onLoad'
62
- })
63
+ }),
64
+ userPreferenceDetailPanel()
63
65
  );
64
66
  }
65
67
  });
@@ -1,23 +1,6 @@
1
1
  .xh-admin-role-details {
2
2
  &__form {
3
- overflow-y: auto;
4
-
5
- .xh-form-field {
6
- margin: 0;
7
- padding: 2px var(--xh-pad-px);
8
-
9
- &__label {
10
- font-size: var(--xh-font-size-small-px);
11
- padding: 0;
12
- }
13
-
14
- &__readonly-display {
15
- padding: var(--xh-pad-half-px);
16
- }
17
-
18
- &:nth-child(odd) {
19
- background-color: var(--xh-grid-bg-odd);
20
- }
21
- }
3
+ flex: none;
4
+ margin: var(--xh-pad-px);
22
5
  }
23
6
  }
@@ -4,8 +4,9 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
- import {form} from '@xh/hoist/cmp/form';
8
- import {div, frame, placeholder, span, vframe} from '@xh/hoist/cmp/layout';
7
+ import {valOrNa} from '@xh/hoist/admin/AdminUtils';
8
+ import {form, formFieldSet} from '@xh/hoist/cmp/form';
9
+ import {frame, hbox, placeholder, vframe} from '@xh/hoist/cmp/layout';
9
10
  import {tabContainer} from '@xh/hoist/cmp/tab';
10
11
  import {creates, hoistCmp} from '@xh/hoist/core';
11
12
  import {formField} from '@xh/hoist/desktop/cmp/form';
@@ -31,21 +32,20 @@ export const roleDetails = hoistCmp.factory({
31
32
 
32
33
  const details = hoistCmp.factory(() =>
33
34
  form({
34
- fieldDefaults: {inline: true},
35
- item: div({
36
- className: 'xh-admin-role-details__form',
35
+ fieldDefaults: {readonlyRenderer: valOrNa},
36
+ item: formFieldSet({
37
+ title: 'Role',
38
+ className: 'xh-admin-role-details__form xh-admin-readonly-form',
37
39
  items: [
38
- formField({field: 'name'}),
39
- formField({field: 'category'}),
40
+ hbox(formField({field: 'name', flex: 1}), formField({field: 'category', flex: 1})),
40
41
  formField({
41
42
  field: 'notes',
42
- readonlyRenderer: v => {
43
- return frame({
43
+ readonlyRenderer: v =>
44
+ frame({
44
45
  style: {overflowY: 'auto'},
45
- height: 50,
46
- item: v ?? span({item: 'N/A', className: 'xh-text-color-muted'})
47
- });
48
- }
46
+ maxHeight: 80,
47
+ item: valOrNa(v)
48
+ })
49
49
  }),
50
50
  formField({field: 'lastUpdated'})
51
51
  ]
@@ -17,6 +17,7 @@ import {Icon} from '../icon';
17
17
  */
18
18
  export class AboutDialogModel extends HoistModel {
19
19
  override xhImpl = true;
20
+ override xhName = 'aboutDialogModel';
20
21
 
21
22
  @observable
22
23
  isOpen: boolean = false;
@@ -71,6 +71,8 @@ import {installServicesAsync} from '../core/impl/InstallServices';
71
71
  * Root object for Framework GUI State.
72
72
  */
73
73
  export class AppContainerModel extends HoistModel {
74
+ override xhName = 'appContainerModel';
75
+
74
76
  override telemetryPrefix = 'xh.client';
75
77
 
76
78
  private initCalled = false;
@@ -338,6 +340,7 @@ export class AppContainerModel extends HoistModel {
338
340
  // App init phase
339
341
  this.setAppState('INITIALIZING_APP');
340
342
  this.appModel = createSingleton(this.appSpec.modelClass);
343
+ this.appModel.xhName ??= 'appModel';
341
344
  await this.runner(ctx)
342
345
  .span('appInit')
343
346
  .run(ctx => this.appModel.initAsync(ctx as InitContext));
@@ -16,6 +16,7 @@ import {camelCase, isBoolean, isString, mapKeys, pick} from 'lodash';
16
16
  */
17
17
  export class AppStateModel extends HoistModel {
18
18
  override xhImpl = true;
19
+ override xhName = 'appStateModel';
19
20
 
20
21
  @observable state: AppState = 'PRE_AUTH';
21
22
 
@@ -17,6 +17,7 @@ import {BannerModel} from './BannerModel';
17
17
  */
18
18
  export class BannerSourceModel extends HoistModel {
19
19
  override xhImpl = true;
20
+ override xhName = 'bannerSourceModel';
20
21
 
21
22
  @managed
22
23
  @observable.ref
@@ -10,6 +10,7 @@ import {throwIf} from '@xh/hoist/utils/js';
10
10
 
11
11
  export class ChangelogDialogModel extends HoistModel {
12
12
  override xhImpl = true;
13
+ override xhName = 'changelogDialogModel';
13
14
 
14
15
  @observable isOpen: boolean = false;
15
16
 
@@ -15,6 +15,7 @@ import {bindable, makeObservable} from '@xh/hoist/mobx';
15
15
  */
16
16
  export class ColChooserOptionsModel extends HoistModel {
17
17
  override xhImpl = true;
18
+ override xhName = 'colChooserOptionsModel';
18
19
  override persistWith = {localStorageKey: 'xhColChooser'};
19
20
 
20
21
  @bindable showGroups: boolean = true;
@@ -18,6 +18,7 @@ import {action, observable, makeObservable, bindable} from '@xh/hoist/mobx';
18
18
  */
19
19
  export class ExceptionDialogModel extends HoistModel {
20
20
  override xhImpl = true;
21
+ override xhName = 'exceptionDialogModel';
21
22
 
22
23
  @observable.ref
23
24
  displayData: {exception: HoistException; options: ExceptionHandlerOptions};
@@ -14,6 +14,7 @@ import {stripTags} from '@xh/hoist/utils/js';
14
14
  */
15
15
  export class FeedbackDialogModel extends HoistModel {
16
16
  override xhImpl = true;
17
+ override xhName = 'feedbackDialogModel';
17
18
 
18
19
  @observable isOpen: boolean = false;
19
20
  @observable message: string = null;