@xh/hoist 78.0.0-SNAPSHOT.1762201137843 → 78.0.0-SNAPSHOT.1762285014309

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## 78.0.0-SNAPSHOT - unreleased
4
4
 
5
+ ### 🎁 New Features
6
+ * New method `StoreRecord.getModifiedValues()` to gather edited data from a store record.
7
+
8
+ ### 🐞 Bug Fixes
9
+ * StoreRecord will no longer report `isModified` as `true` if a field has been edited and
10
+ then returned to its original value in a subsequent edit.
11
+
5
12
  ## 77.0.1 - 2025-10-29
6
13
 
7
14
  ### 💥 Breaking Changes
@@ -7,7 +7,7 @@
7
7
  import {exportFilenameWithDate} from '@xh/hoist/admin/AdminUtils';
8
8
  import {AppModel} from '@xh/hoist/admin/AppModel';
9
9
  import * as Col from '@xh/hoist/admin/columns';
10
- import {hbox, hspacer} from '@xh/hoist/cmp/layout';
10
+ import {br, fragment, hbox, hspacer} from '@xh/hoist/cmp/layout';
11
11
  import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
12
12
  import {FieldSpec} from '@xh/hoist/data';
13
13
  import {defaultReadonlyRenderer} from '@xh/hoist/desktop/cmp/form';
@@ -17,14 +17,14 @@ import {
17
17
  cloneAction,
18
18
  deleteAction,
19
19
  editAction,
20
- RestGridModel,
21
- RestStore
20
+ RestGridModel
22
21
  } from '@xh/hoist/desktop/cmp/rest';
22
+ import {Icon} from '@xh/hoist/icon';
23
23
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
24
+ import {pluralize} from '@xh/hoist/utils/js';
24
25
  import {isNil, truncate} from 'lodash';
25
26
  import {DifferModel} from '../../../differ/DifferModel';
26
27
  import {RegroupDialogModel} from '../../../regroup/RegroupDialogModel';
27
- import {Icon} from '@xh/hoist/icon';
28
28
 
29
29
  export class ConfigPanelModel extends HoistModel {
30
30
  override persistWith = {localStorageKey: 'xhAdminConfigState'};
@@ -43,18 +43,27 @@ export class ConfigPanelModel extends HoistModel {
43
43
  super();
44
44
  makeObservable(this);
45
45
 
46
- const required = true,
46
+ const {regroupAction} = this.regroupDialogModel,
47
+ required = true,
47
48
  enableCreate = true,
48
49
  hidden = true;
49
50
 
50
51
  this.gridModel = new RestGridModel({
51
- readonly: AppModel.readonly,
52
- persistWith: this.persistWith,
52
+ // Core config
53
+ autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
53
54
  colChooserModel: true,
54
55
  enableExport: true,
55
56
  exportOptions: {filename: exportFilenameWithDate('configs')},
57
+ filterFields: ['name', 'value', 'groupName', 'note'],
58
+ groupBy: 'groupName',
59
+ persistWith: this.persistWith,
60
+ prepareCloneFn: ({clone}) => (clone.name = `${clone.name}_CLONE`),
61
+ readonly: AppModel.readonly,
56
62
  selModel: 'multiple',
57
- store: new RestStore({
63
+ sortBy: 'name',
64
+ unit: 'config',
65
+ // Store + fields
66
+ store: {
58
67
  url: 'rest/configAdmin',
59
68
  reloadLookupsOnLoad: true,
60
69
  fieldDefaults: {enableXssProtection: false},
@@ -83,25 +92,8 @@ export class ConfigPanelModel extends HoistModel {
83
92
  editable: false
84
93
  }
85
94
  ]
86
- }),
87
- actionWarning: {
88
- del: records =>
89
- `Are you sure you want to delete ${records.length} config(s)? Deleting configs can break running apps.`
90
95
  },
91
- toolbarActions: [addAction, editAction, cloneAction, deleteAction],
92
- menuActions: [
93
- addAction,
94
- editAction,
95
- cloneAction,
96
- deleteAction,
97
- '-',
98
- this.regroupDialogModel.regroupAction
99
- ],
100
- prepareCloneFn: ({clone}) => (clone.name = `${clone.name}_CLONE`),
101
- unit: 'config',
102
- filterFields: ['name', 'value', 'groupName', 'note'],
103
- sortBy: 'name',
104
- groupBy: 'groupName',
96
+ // Cols + editors
105
97
  columns: [
106
98
  {...Col.groupName, hidden},
107
99
  {...Col.name},
@@ -138,7 +130,19 @@ export class ConfigPanelModel extends HoistModel {
138
130
  {field: 'clientVisible'},
139
131
  {field: 'lastUpdated'},
140
132
  {field: 'lastUpdatedBy'}
141
- ]
133
+ ],
134
+ // Actions
135
+ actionWarning: {
136
+ del: records =>
137
+ fragment(
138
+ `Are you sure you want to delete ${pluralize('selected config', records.length, true)}?`,
139
+ br(),
140
+ br(),
141
+ `Deleting configs can break running apps.`
142
+ )
143
+ },
144
+ menuActions: [addAction, editAction, cloneAction, deleteAction, '-', regroupAction],
145
+ toolbarActions: [addAction, editAction, cloneAction, deleteAction]
142
146
  });
143
147
  }
144
148
 
@@ -25,12 +25,19 @@ export class UserPreferenceModel extends HoistModel {
25
25
  hidden = true;
26
26
 
27
27
  this.gridModel = new RestGridModel({
28
- readonly: AppModel.readonly,
29
- persistWith: {localStorageKey: 'xhAdminUserPreferenceState'},
28
+ // Core config
29
+ autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
30
30
  colChooserModel: true,
31
31
  enableExport: true,
32
32
  exportOptions: {filename: exportFilenameWithDate('user-prefs')},
33
+ filterFields: ['name', 'username'],
34
+ groupBy: 'groupName',
35
+ persistWith: {localStorageKey: 'xhAdminUserPreferenceState'},
36
+ readonly: AppModel.readonly,
33
37
  selModel: 'multiple',
38
+ sortBy: 'name',
39
+ unit: 'user preference',
40
+ // Store + fields
34
41
  store: {
35
42
  url: 'rest/userPreferenceAdmin',
36
43
  reloadLookupsOnLoad: true,
@@ -55,10 +62,7 @@ export class UserPreferenceModel extends HoistModel {
55
62
  {...(Col.lastUpdatedBy.field as FieldSpec), editable: false}
56
63
  ]
57
64
  },
58
- sortBy: 'name',
59
- groupBy: 'groupName',
60
- unit: 'user preference',
61
- filterFields: ['name', 'username'],
65
+ // Cols + editors
62
66
  columns: [
63
67
  {...Col.name},
64
68
  {...Col.type},
@@ -7,11 +7,13 @@
7
7
  import {exportFilenameWithDate} from '@xh/hoist/admin/AdminUtils';
8
8
  import {AppModel} from '@xh/hoist/admin/AppModel';
9
9
  import * as Col from '@xh/hoist/admin/columns';
10
+ import {br, fragment} from '@xh/hoist/cmp/layout';
10
11
  import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
11
12
  import {FieldSpec} from '@xh/hoist/data';
12
13
  import {textArea} from '@xh/hoist/desktop/cmp/input';
13
14
  import {addAction, deleteAction, editAction, RestGridModel} from '@xh/hoist/desktop/cmp/rest';
14
15
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
16
+ import {pluralize} from '@xh/hoist/utils/js';
15
17
  import {DifferModel} from '../../../../differ/DifferModel';
16
18
  import {RegroupDialogModel} from '../../../../regroup/RegroupDialogModel';
17
19
 
@@ -32,18 +34,26 @@ export class PrefEditorModel extends HoistModel {
32
34
  super();
33
35
  makeObservable(this);
34
36
 
35
- const required = true,
37
+ const {regroupAction} = this.regroupDialogModel,
38
+ required = true,
36
39
  enableCreate = true,
37
40
  hidden = true;
38
41
 
39
42
  this.gridModel = new RestGridModel({
40
- readonly: AppModel.readonly,
41
- persistWith: this.persistWith,
43
+ // Core config
44
+ autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
42
45
  colChooserModel: true,
43
46
  enableExport: true,
44
47
  exportOptions: {filename: exportFilenameWithDate('prefs')},
48
+ filterFields: ['name', 'groupName'],
49
+ groupBy: 'groupName',
50
+ persistWith: this.persistWith,
51
+ readonly: AppModel.readonly,
45
52
  selModel: 'multiple',
46
53
  showRefreshButton: true,
54
+ sortBy: 'name',
55
+ unit: 'preference',
56
+ // Store + fields
47
57
  store: {
48
58
  url: 'rest/preferenceAdmin',
49
59
  reloadLookupsOnLoad: true,
@@ -68,21 +78,7 @@ export class PrefEditorModel extends HoistModel {
68
78
  {...(Col.lastUpdatedBy.field as FieldSpec), editable: false}
69
79
  ]
70
80
  },
71
- sortBy: 'name',
72
- groupBy: 'groupName',
73
- unit: 'preference',
74
- filterFields: ['name', 'groupName'],
75
- actionWarning: {
76
- del: records =>
77
- `Are you sure you want to delete ${records.length} preference(s)? Deleting preferences can break running apps.`
78
- },
79
- menuActions: [
80
- addAction,
81
- editAction,
82
- deleteAction,
83
- '-',
84
- this.regroupDialogModel.regroupAction
85
- ],
81
+ // Cols + Editors
86
82
  columns: [
87
83
  {...Col.name},
88
84
  {...Col.type},
@@ -100,7 +96,18 @@ export class PrefEditorModel extends HoistModel {
100
96
  {field: 'notes', formField: {item: textArea({height: 100})}},
101
97
  {field: 'lastUpdated'},
102
98
  {field: 'lastUpdatedBy'}
103
- ]
99
+ ],
100
+ // Actions
101
+ actionWarning: {
102
+ del: records =>
103
+ fragment(
104
+ `Are you sure you want to delete ${pluralize('selected preference', records.length, true)}?`,
105
+ br(),
106
+ br(),
107
+ `Deleting preference definitions can break running apps.`
108
+ )
109
+ },
110
+ menuActions: [addAction, editAction, deleteAction, '-', regroupAction]
104
111
  });
105
112
  }
106
113
 
@@ -94,6 +94,14 @@ export declare class StoreRecord {
94
94
  * Field in the Store. Useful for cloning/iterating over all values (including defaults).
95
95
  */
96
96
  getValues(): PlainObject;
97
+ /**
98
+ * Get a map of modified values only.
99
+ *
100
+ * If record has no modifications, this method will return null.
101
+ * If modifications are returned, the returned object will include id,
102
+ * for convenience.
103
+ */
104
+ getModifiedValues(): PlainObject;
97
105
  /**
98
106
  * Construct a StoreRecord from a pre-processed `data` source object.
99
107
  *
@@ -1,10 +1,11 @@
1
- import { RowDoubleClickedEvent } from '@xh/hoist/kit/ag-grid';
2
1
  import { BaseFieldConfig } from '@xh/hoist/cmp/form';
3
2
  import { GridConfig, GridModel } from '@xh/hoist/cmp/grid';
4
3
  import { ElementSpec, HoistModel, PlainObject } from '@xh/hoist/core';
5
4
  import '@xh/hoist/desktop/register';
6
5
  import { RecordAction, RecordActionSpec, StoreRecord } from '@xh/hoist/data';
6
+ import { RowDoubleClickedEvent } from '@xh/hoist/kit/ag-grid';
7
7
  import { ExportOptions } from '@xh/hoist/svc';
8
+ import { ReactNode } from 'react';
8
9
  import { FormFieldProps } from '../form';
9
10
  import { RestStore, RestStoreConfig } from './data/RestStore';
10
11
  import { RestFormModel } from './impl/RestFormModel';
@@ -22,9 +23,9 @@ export interface RestGridConfig extends GridConfig {
22
23
  showRefreshButton?: boolean;
23
24
  /** Warning to display before actions on a selection of records. */
24
25
  actionWarning?: {
25
- add?: string | ((recs: StoreRecord[]) => string);
26
- del?: string | ((recs: StoreRecord[]) => string);
27
- edit?: string | ((recs: StoreRecord[]) => string);
26
+ add?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
27
+ del?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
28
+ edit?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
28
29
  };
29
30
  /** Name that describes records in this grid. */
30
31
  unit?: string;
@@ -16,9 +16,9 @@ export declare class RestFormModel extends HoistModel {
16
16
  dialogRef: import("react").RefObject<HTMLElement>;
17
17
  get unit(): string;
18
18
  get actionWarning(): {
19
- add?: string | ((recs: import("@xh/hoist/data").StoreRecord[]) => string);
20
- del?: string | ((recs: import("@xh/hoist/data").StoreRecord[]) => string);
21
- edit?: string | ((recs: import("@xh/hoist/data").StoreRecord[]) => string);
19
+ add?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
20
+ del?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
21
+ edit?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
22
22
  };
23
23
  get actions(): (import("@xh/hoist/data").RecordActionSpec | import("@xh/hoist/data").RecordAction)[];
24
24
  get editors(): RestGridEditor[];
package/data/Store.ts CHANGED
@@ -515,10 +515,12 @@ export class Store extends HoistBase {
515
515
 
516
516
  return new StoreRecord({
517
517
  id,
518
- data: parsedData,
519
518
  store: this,
519
+ raw: null,
520
+ data: parsedData,
521
+ committedData: null,
520
522
  parent,
521
- committedData: null
523
+ isSummary: false
522
524
  });
523
525
  });
524
526
 
@@ -585,13 +587,22 @@ export class Store extends HoistBase {
585
587
  const currentRec = this.getOrThrow(id),
586
588
  updatedData = this.parseUpdate(currentRec.data, mod);
587
589
 
590
+ // If after parsing, data is deep equal, its a no-op
591
+ if (equal(updatedData, currentRec.data)) return;
592
+
593
+ // Previously updated record might now be reverted to clean, normalize
594
+ const committedData =
595
+ currentRec.isModified && equal(currentRec.committedData, updatedData)
596
+ ? updatedData
597
+ : currentRec.committedData;
598
+
588
599
  const updatedRec = new StoreRecord({
589
600
  id: currentRec.id,
601
+ store: currentRec.store,
590
602
  raw: currentRec.raw,
591
603
  data: updatedData,
604
+ committedData: committedData,
592
605
  parent: currentRec.parent,
593
- store: currentRec.store,
594
- committedData: currentRec.committedData,
595
606
  isSummary: currentRec.isSummary
596
607
  });
597
608
 
@@ -1036,7 +1047,15 @@ export class Store extends HoistBase {
1036
1047
  }
1037
1048
 
1038
1049
  data = this.parseRaw(data);
1039
- const ret = new StoreRecord({id, data, raw, parent, store: this, isSummary});
1050
+ const ret = new StoreRecord({
1051
+ id,
1052
+ store: this,
1053
+ raw,
1054
+ data,
1055
+ committedData: data,
1056
+ parent,
1057
+ isSummary
1058
+ });
1040
1059
 
1041
1060
  // Finalize summary only. Non-summary finalized by RecordSet
1042
1061
  if (isSummary) ret.finalize();
@@ -1093,7 +1112,7 @@ export class Store extends HoistBase {
1093
1112
  return ret;
1094
1113
  }
1095
1114
 
1096
- private parseUpdate(data, update) {
1115
+ private parseUpdate(data: PlainObject, update: PlainObject): PlainObject {
1097
1116
  const {_fieldMap} = this;
1098
1117
 
1099
1118
  // a) clone the existing object
@@ -1151,9 +1170,11 @@ export class Store extends HoistBase {
1151
1170
 
1152
1171
  const ret = new StoreRecord({
1153
1172
  id: recToRevert.id,
1154
- raw: recToRevert.raw,
1155
- data: {...recToRevert.committedData},
1156
1173
  store: this,
1174
+ raw: recToRevert.raw,
1175
+ data: recToRevert.committedData,
1176
+ committedData: recToRevert.committedData,
1177
+ parent: null,
1157
1178
  isSummary: true
1158
1179
  });
1159
1180
  ret.finalize();
@@ -6,11 +6,12 @@
6
6
  */
7
7
  import {PlainObject} from '@xh/hoist/core';
8
8
  import {throwIf} from '@xh/hoist/utils/js';
9
- import {isNil, flatMap, isMatch} from 'lodash';
9
+ import {isNil, flatMap, isMatch, isEmpty, pickBy} from 'lodash';
10
10
  import {Store} from './Store';
11
11
  import {ValidationState} from './validation/ValidationState';
12
12
  import {RecordValidator} from './impl/RecordValidator';
13
13
  import {Field} from './Field';
14
+ import equal from 'fast-deep-equal';
14
15
 
15
16
  /**
16
17
  * Wrapper object for each data element within a {@link Store}. Records must be assigned a unique ID
@@ -184,6 +185,26 @@ export class StoreRecord {
184
185
  return ret;
185
186
  }
186
187
 
188
+ /**
189
+ * Get a map of modified values only.
190
+ *
191
+ * If record has no modifications, this method will return null.
192
+ * If modifications are returned, the returned object will include id,
193
+ * for convenience.
194
+ */
195
+ getModifiedValues(): PlainObject {
196
+ if (!this.isModified) return null;
197
+
198
+ const {data, committedData} = this,
199
+ ret = pickBy(data, (v, k) => !equal(v, committedData[k]));
200
+ if (!isEmpty(ret)) {
201
+ ret.id = this.id;
202
+ return ret;
203
+ } else {
204
+ return null;
205
+ }
206
+ }
207
+
187
208
  /**
188
209
  * Construct a StoreRecord from a pre-processed `data` source object.
189
210
  *
@@ -195,15 +216,7 @@ export class StoreRecord {
195
216
  * @internal
196
217
  */
197
218
  constructor(config: StoreRecordConfig) {
198
- const {
199
- id,
200
- store,
201
- data,
202
- raw = null,
203
- committedData = data,
204
- parent,
205
- isSummary = false
206
- } = config;
219
+ const {id, store, raw, data, committedData, parent, isSummary} = config;
207
220
  throwIf(
208
221
  isNil(id),
209
222
  "Record needs an ID. Use 'Store.idSpec' to specify a unique ID for each record."
package/data/cube/View.ts CHANGED
@@ -373,7 +373,7 @@ export class View extends HoistBase {
373
373
  if (isNil(bucketVal)) {
374
374
  ret.push(row);
375
375
  } else {
376
- const bucketRows = buckets[bucketVal] ??= [];
376
+ const bucketRows = (buckets[bucketVal] ??= []);
377
377
  bucketRows.push(row);
378
378
  }
379
379
  });
@@ -5,15 +5,16 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- import {RowDoubleClickedEvent} from '@xh/hoist/kit/ag-grid';
9
8
  import {BaseFieldConfig} from '@xh/hoist/cmp/form';
10
9
  import {GridConfig, GridModel} from '@xh/hoist/cmp/grid';
11
10
  import {ElementSpec, HoistModel, managed, PlainObject, XH} from '@xh/hoist/core';
12
11
  import '@xh/hoist/desktop/register';
13
12
  import {RecordAction, RecordActionSpec, StoreRecord} from '@xh/hoist/data';
13
+ import {RowDoubleClickedEvent} from '@xh/hoist/kit/ag-grid';
14
14
  import {ExportOptions} from '@xh/hoist/svc';
15
15
  import {pluralize, throwIf, withDefault} from '@xh/hoist/utils/js';
16
16
  import {isFunction} from 'lodash';
17
+ import {ReactNode} from 'react';
17
18
  import {FormFieldProps} from '../form';
18
19
  import {addAction, deleteAction, editAction, viewAction} from './Actions';
19
20
  import {RestStore, RestStoreConfig} from './data/RestStore';
@@ -39,9 +40,9 @@ export interface RestGridConfig extends GridConfig {
39
40
 
40
41
  /** Warning to display before actions on a selection of records. */
41
42
  actionWarning?: {
42
- add?: string | ((recs: StoreRecord[]) => string);
43
- del?: string | ((recs: StoreRecord[]) => string);
44
- edit?: string | ((recs: StoreRecord[]) => string);
43
+ add?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
44
+ del?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
45
+ edit?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
45
46
  };
46
47
 
47
48
  /** Name that describes records in this grid. */
@@ -109,11 +110,7 @@ export class RestGridModel extends HoistModel {
109
110
  add: null,
110
111
  edit: null,
111
112
  del: recs =>
112
- recs.length > 1
113
- ? `Are you sure you want to delete the selected ${recs.length} ${pluralize(
114
- this.unit
115
- )}?`
116
- : `Are you sure you want to delete the selected ${this.unit}?`
113
+ `Are you sure you want to delete ${pluralize(`selected ${this.unit}`, recs.length, true)}?`
117
114
  };
118
115
 
119
116
  @managed gridModel: GridModel = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "78.0.0-SNAPSHOT.1762201137843",
3
+ "version": "78.0.0-SNAPSHOT.1762285014309",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",