@onehat/ui 0.3.287 → 0.3.290

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/ui",
3
- "version": "0.3.287",
3
+ "version": "0.3.290",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,38 @@
1
+ import {
2
+ Icon,
3
+ } from 'native-base';
4
+ import testProps from '../../Functions/testProps.js';
5
+ import IconButton from './IconButton.js';
6
+ import Rotate from '../Icons/Rotate.js';
7
+
8
+ export default function ReloadButton(props) {
9
+ const {
10
+ iconProps ={},
11
+ self,
12
+ Repository,
13
+ isTree = false,
14
+ } = props,
15
+ onPress = () => {
16
+ if (isTree) {
17
+ Repository.loadRootNodes(1);
18
+ } else {
19
+ Repository.reload();
20
+ }
21
+ };
22
+
23
+ if (!Repository || Repository.isLocal) {
24
+ return null;
25
+ }
26
+
27
+ return <IconButton
28
+ {...testProps('reloadBtn')}
29
+ {...props}
30
+ reference="reloadBtn"
31
+ parent={self}
32
+ icon={<Icon as={Rotate} {...iconProps} color="trueGray.600" />}
33
+ onPress={onPress}
34
+ tooltip="Reload"
35
+ ml={2}
36
+ />;
37
+ }
38
+
@@ -1,30 +1,6 @@
1
- import {
2
- Icon,
3
- } from 'native-base';
4
- import testProps from '../../Functions/testProps.js';
5
- import IconButton from './IconButton.js';
6
- import Rotate from '../Icons/Rotate.js';
1
+ import ReloadButton from './ReloadButton.js';
7
2
 
8
3
  export default function ReloadPageButton(props) {
9
- const {
10
- iconProps ={},
11
- self,
12
- Repository,
13
- } = props;
14
-
15
- if (!Repository || Repository.isLocal) {
16
- return null;
17
- }
18
-
19
- return <IconButton
20
- {...testProps('reloadPageBtn')}
21
- {...props}
22
- reference="reloadPageBtn"
23
- parent={self}
24
- icon={<Icon as={Rotate} {...iconProps} color="trueGray.600" />}
25
- onPress={() => Repository.reload()}
26
- tooltip="Reload"
27
- ml={2}
28
- />;
4
+ return <ReloadButton isTree={false} {...props} />;
29
5
  }
30
6
 
@@ -0,0 +1,6 @@
1
+ import ReloadButton from './ReloadButton.js';
2
+
3
+ export default function ReloadTreeButton(props) {
4
+ return <ReloadButton isTree={true} {...props} />;
5
+ }
6
+
@@ -134,8 +134,8 @@ function Form(props) {
134
134
  }
135
135
  const
136
136
  isMultiple = _.isArray(record),
137
- isSingle = !isMultiple, // for convenience
138
- isPhantom = !skipAll && !!record?.isPhantom, //
137
+ isSingle = _.isNil(record) || !_.isArray(record),
138
+ isPhantom = !skipAll && !!record?.isPhantom,
139
139
  forceUpdate = useForceUpdate(),
140
140
  [previousRecord, setPreviousRecord] = useState(record),
141
141
  [containerWidth, setContainerWidth] = useState(),
@@ -949,6 +949,9 @@ function Form(props) {
949
949
  const formIsDirty = formState.isDirty;
950
950
  // console.log('formIsDirty', formIsDirty);
951
951
  // console.log('isPhantom', isPhantom);
952
+ if (editorType === EDITOR_TYPE__WINDOWED && onCancel) {
953
+ showCancelBtn = true;
954
+ }
952
955
  if (formIsDirty || isPhantom) {
953
956
  if (isSingle && onCancel) {
954
957
  showCancelBtn = true;
@@ -976,13 +976,6 @@ function GridComponent(props) {
976
976
  </Toolbar>;
977
977
  }
978
978
  }
979
-
980
- const sizeProps = {};
981
- if (!_.isNil(h)) {
982
- sizeProps.h = h;
983
- } else {
984
- sizeProps.flex = flex ?? 1;
985
- }
986
979
 
987
980
  let grid = <FlatList
988
981
  {...testProps('flatlist')}
@@ -1064,6 +1057,13 @@ function GridComponent(props) {
1064
1057
  />
1065
1058
  </Modal>;
1066
1059
  }
1060
+
1061
+ const sizeProps = {};
1062
+ if (!_.isNil(h)) {
1063
+ sizeProps.h = h;
1064
+ } else {
1065
+ sizeProps.flex = flex ?? 1;
1066
+ }
1067
1067
 
1068
1068
  grid = <Column
1069
1069
  {...testProps(self)}
@@ -142,7 +142,9 @@ export default function withEditor(WrappedComponent, isTree = false) {
142
142
  if (!selection[0]) {
143
143
  throw Error('Must select a parent node.');
144
144
  }
145
- addValues.parentId = selection[0].id;
145
+ const parent = selection[0];
146
+ addValues.parentId = parent.id;
147
+ addValues.depth = parent.depth +1;
146
148
  } else {
147
149
  // Set repository to sort by id DESC and switch to page 1, so this new entity is guaranteed to show up on the current page, even after saving
148
150
  const currentSorter = Repository.sorters[0];
@@ -173,10 +175,13 @@ export default function withEditor(WrappedComponent, isTree = false) {
173
175
  setIsSaving(false);
174
176
  setIsIgnoreNextSelectionChange(true);
175
177
  setSelection([entity]);
178
+ if (getListeners().onAfterAdd) {
179
+ await getListeners().onAfterAdd(entity);
180
+ }
176
181
  if (Repository.isAutoSave) {
177
182
  // for isAutoSave Repositories, submit the handers right away
178
- if (getListeners().onAfterAdd) {
179
- await getListeners().onAfterAdd(entity);
183
+ if (getListeners().onAfterAddSave) {
184
+ await getListeners().onAfterAddSave(selection);
180
185
  }
181
186
  if (onAdd) {
182
187
  await onAdd(entity);
@@ -377,8 +382,8 @@ export default function withEditor(WrappedComponent, isTree = false) {
377
382
  if (onAdd) {
378
383
  await onAdd(selection);
379
384
  }
380
- if (getListeners().onAfterAdd) {
381
- await getListeners().onAfterAdd(selection);
385
+ if (getListeners().onAfterAddSave) {
386
+ await getListeners().onAfterAddSave(selection);
382
387
  }
383
388
  setIsAdding(false);
384
389
  setEditorMode(EDITOR_MODE__EDIT);
@@ -67,5 +67,5 @@ export default function withSideEditor(WrappedComponent, isTree = false) {
67
67
  />}
68
68
  />;
69
69
  };
70
- return withAdditionalProps(withEditor(SideEditor));
70
+ return withAdditionalProps(withEditor(SideEditor, isTree));
71
71
  }
@@ -39,6 +39,7 @@ import getIconButtonFromConfig from '../../Functions/getIconButtonFromConfig.js'
39
39
  import inArray from '../../Functions/inArray.js';
40
40
  import testProps from '../../Functions/testProps.js';
41
41
  import nbToRgb from '../../Functions/nbToRgb.js';
42
+ import ReloadTreeButton from '../Buttons/ReloadTreeButton.js';
42
43
  import TreeNode, { DraggableTreeNode } from './TreeNode.js';
43
44
  import FormPanel from '../Panel/FormPanel.js';
44
45
  import Input from '../Form/Field/Input.js';
@@ -61,6 +62,7 @@ const DEPTH_INDENT_PX = 25;
61
62
  function TreeComponent(props) {
62
63
  const {
63
64
  areRootsVisible = true,
65
+ autoLoadRootNodes = true,
64
66
  extraParams = {}, // Additional params to send with each request ( e.g. { order: 'Categories.name ASC' })
65
67
  getNodeText = (item) => { // extracts model/data and decides what the row text should be
66
68
  if (Repository) {
@@ -68,6 +70,9 @@ function TreeComponent(props) {
68
70
  }
69
71
  return item[displayIx];
70
72
  },
73
+ getDisplayTextFromSearchResults = (item) => {
74
+ return item.id
75
+ },
71
76
  getNodeIcon = (which, item) => { // decides what icon to show for this node
72
77
  // TODO: Allow for dynamic props on the icon (e.g. special color for some icons)
73
78
  let icon;
@@ -232,6 +237,9 @@ function TreeComponent(props) {
232
237
  }
233
238
  },
234
239
  onBeforeAdd = async () => {
240
+ // called during withEditor::doAdd, before the add operation is called
241
+ // returning false will cancel the add operation
242
+
235
243
  // Load children before adding the new node
236
244
  if (_.isEmpty(selection)) {
237
245
  alert('Please select a parent node first.')
@@ -244,24 +252,32 @@ function TreeComponent(props) {
244
252
  if (parent.hasChildren && !parent.areChildrenLoaded) {
245
253
  await loadChildren(parentDatum);
246
254
  }
255
+
256
+ // forceUpdate();
257
+ },
258
+ onAfterAdd = (entity) => {
259
+ // called during withEditor::doAdd, after the add operation is called
260
+
261
+ // Add the entity to the tree, show parent as hasChildren and expanded
262
+ const
263
+ parent = selection[0],
264
+ parentDatum = getNodeData(parent.id);
265
+ if (!parent.hasChildren) {
266
+ parent.hasChildren = true; // since we're adding a new child
267
+ }
247
268
  if (!parentDatum.isExpanded) {
248
269
  parentDatum.isExpanded = true;
249
270
  }
271
+
272
+ buildRowToDatumMap();
250
273
  forceUpdate();
251
274
  },
252
- onAfterAdd = async (entities) => {
253
- // Expand the parent before showing the new node
254
- const
255
- entity = entities[0],
256
- parentDatum = getNodeData(entity.parentId);
257
-
275
+ onAfterAddSave = (entities) => {
258
276
 
259
- // Add the entity to the tree
260
- const entityDatum = buildTreeNodeDatum(entity);
261
- parentDatum.children.unshift(entityDatum);
262
- forceUpdate();
277
+ // Update the datum with the new entity
278
+ return onAfterEdit(entities);
263
279
 
264
- buildRowToDatumMap();
280
+
265
281
  },
266
282
  onBeforeEditSave = (entities) => {
267
283
  onBeforeSave(entities);
@@ -370,7 +386,7 @@ function TreeComponent(props) {
370
386
  // Show modal so user can select which node to go to
371
387
  const searchFormData = [];
372
388
  _.each(found, (item) => {
373
- searchFormData.push([item.id, getNodeText(item)]);
389
+ searchFormData.push([item.id, getDisplayTextFromSearchResults(item)]);
374
390
  });
375
391
  setSearchFormData(searchFormData);
376
392
  setSearchResults(found);
@@ -686,8 +702,9 @@ function TreeComponent(props) {
686
702
 
687
703
  currentNode = currentDatum.item;
688
704
 
689
- // THE MAGIC!
690
- currentDatum.isExpanded = true;
705
+ if (!currentDatum.isExpanded) {
706
+ await loadChildren(currentDatum, 1);
707
+ }
691
708
 
692
709
  cPath = cPathParts.slice(1).join('/'); // put the rest of it back together
693
710
  currentLevelData = currentDatum.children;
@@ -910,6 +927,7 @@ function TreeComponent(props) {
910
927
  onToggle={onToggle}
911
928
  isDragMode={isDragMode}
912
929
  isHighlighted={highlitedDatum === datum}
930
+ isSelected={isSelected}
913
931
 
914
932
  // fields={fields}
915
933
  />;
@@ -1102,6 +1120,10 @@ function TreeComponent(props) {
1102
1120
  Repository.setBaseParams(extraParams);
1103
1121
  }
1104
1122
 
1123
+ if (autoLoadRootNodes) {
1124
+ Repository.loadRootNodes(1);
1125
+ }
1126
+
1105
1127
  async function rebuildTree() {
1106
1128
  setIsReady(false);
1107
1129
  await buildAndSetTreeNodeData();
@@ -1117,6 +1139,7 @@ function TreeComponent(props) {
1117
1139
  Repository.on('load', setFalse);
1118
1140
  Repository.on('loadRootNodes', setFalse);
1119
1141
  Repository.on('loadRootNodes', rebuildTree);
1142
+ Repository.on('add', rebuildTree);
1120
1143
  Repository.on('changeFilters', reloadTree);
1121
1144
  Repository.on('changeSorters', reloadTree);
1122
1145
 
@@ -1125,6 +1148,7 @@ function TreeComponent(props) {
1125
1148
  Repository.off('load', setFalse);
1126
1149
  Repository.off('loadRootNodes', setFalse);
1127
1150
  Repository.off('loadRootNodes', rebuildTree);
1151
+ Repository.off('add', rebuildTree);
1128
1152
  Repository.off('changeFilters', reloadTree);
1129
1153
  Repository.off('changeSorters', reloadTree);
1130
1154
  };
@@ -1147,6 +1171,7 @@ function TreeComponent(props) {
1147
1171
  setWithEditListeners({ // Update withEdit's listeners on every render
1148
1172
  onBeforeAdd,
1149
1173
  onAfterAdd,
1174
+ onAfterAddSave,
1150
1175
  onBeforeEditSave,
1151
1176
  onAfterEdit,
1152
1177
  onBeforeDeleteSave,
@@ -1168,9 +1193,16 @@ function TreeComponent(props) {
1168
1193
  let treeFooterComponent = null;
1169
1194
  if (!disableBottomToolbar) {
1170
1195
  if (Repository && bottomToolbar === 'pagination' && !disablePagination && Repository.isPaginated) {
1171
- treeFooterComponent = <PaginationToolbar Repository={Repository} toolbarItems={footerToolbarItemComponents} />;
1196
+ treeFooterComponent = <PaginationToolbar
1197
+ Repository={Repository}
1198
+ self={self}
1199
+ toolbarItems={footerToolbarItemComponents}
1200
+ />;
1172
1201
  } else if (footerToolbarItemComponents.length) {
1173
- treeFooterComponent = <Toolbar>{footerToolbarItemComponents}</Toolbar>;
1202
+ treeFooterComponent = <Toolbar>
1203
+ <ReloadTreeButton Repository={Repository} self={self} />
1204
+ {footerToolbarItemComponents}
1205
+ </Toolbar>;
1174
1206
  }
1175
1207
  }
1176
1208
 
@@ -1207,7 +1239,7 @@ function TreeComponent(props) {
1207
1239
  }
1208
1240
  }}
1209
1241
  >
1210
- <ScrollView flex={1} w="100%">
1242
+ <ScrollView {...testProps('ScrollView')} flex={1} w="100%">
1211
1243
  {!treeNodes?.length ?
1212
1244
  <NoRecordsFound text={noneFoundText} onRefresh={reloadTree} /> :
1213
1245
  treeNodes}
@@ -1224,38 +1256,42 @@ function TreeComponent(props) {
1224
1256
  >
1225
1257
  <Column bg="#fff" w={300}>
1226
1258
  <FormPanel
1227
- title="Choose Tree Node"
1259
+ _panel={{
1260
+ title: 'Choose Tree Node',
1261
+ }}
1228
1262
  instructions="Multiple tree nodes matched your search. Please select which one to show."
1229
- flex={1}
1230
- items={[
1231
- {
1232
- type: 'Column',
1233
- flex: 1,
1234
- items: [
1235
- {
1236
- key: 'node_id',
1237
- name: 'node_id',
1238
- type: 'Combo',
1239
- label: 'Tree Node',
1240
- data: searchFormData,
1241
- }
1242
- ],
1263
+ _form={{
1264
+ flex: 1,
1265
+ items: [
1266
+ {
1267
+ type: 'Column',
1268
+ flex: 1,
1269
+ items: [
1270
+ {
1271
+ key: 'node_id',
1272
+ name: 'node_id',
1273
+ type: 'Combo',
1274
+ label: 'Tree Node',
1275
+ data: searchFormData,
1276
+ }
1277
+ ],
1278
+ },
1279
+ ],
1280
+ onCancel: (e) => {
1281
+ setHighlitedDatum(null);
1282
+ setIsModalShown(false);
1283
+ },
1284
+ onSave: (data, e) => {
1285
+ const
1286
+ treeNode = _.find(searchResults, (item) => {
1287
+ return item.id === data.node_id;
1288
+ }),
1289
+ cPath = treeNode.cPath;
1290
+ expandPath(cPath);
1291
+
1292
+ // Close the modal
1293
+ setIsModalShown(false);
1243
1294
  },
1244
- ]}
1245
- onCancel={(e) => {
1246
- setHighlitedDatum(null);
1247
- setIsModalShown(false);
1248
- }}
1249
- onSave={(data, e) => {
1250
- const
1251
- treeNode = _.find(searchResults, (item) => {
1252
- return item.id === data.node_id;
1253
- }),
1254
- cPath = treeNode.cPath;
1255
- expandPath(cPath);
1256
-
1257
- // Close the modal
1258
- setIsModalShown(false);
1259
1295
  }}
1260
1296
  />
1261
1297
  </Column>
@@ -22,6 +22,7 @@ export default function TreeNode(props) {
22
22
  onToggle,
23
23
  isDragMode,
24
24
  isHighlighted,
25
+ isSelected,
25
26
  ...propsToPass
26
27
  } = props,
27
28
  styles = UiGlobals.styles,
@@ -44,6 +45,7 @@ export default function TreeNode(props) {
44
45
  return useMemo(() => {
45
46
 
46
47
  return <Row
48
+ {...testProps('node' + (isSelected ? '-selected' : ''))}
47
49
  alignItems="center"
48
50
  flexGrow={1}
49
51
  {...nodeProps}
@@ -54,7 +56,7 @@ export default function TreeNode(props) {
54
56
 
55
57
  {isLoading ?
56
58
  <Spinner px={2} /> :
57
- (hasChildren && !isDragMode ? <IconButton icon={icon} onPress={() => onToggle(datum)} {...testProps('TreeNodeExpandBtn-' + item?.id)} /> : <Icon as={icon} px={2} />)}
59
+ (hasChildren && !isDragMode ? <IconButton icon={icon} onPress={() => onToggle(datum)} {...testProps('expandBtn')} /> : <Icon as={icon} px={2} />)}
58
60
 
59
61
  <Text
60
62
  overflow="hidden"
@@ -23,7 +23,7 @@ export function clickDuplicateButton(parentSelectors) {
23
23
  return clickButton(parentSelectors, 'duplicateBtn');
24
24
  }
25
25
  export function clickReloadButton(parentSelectors) {
26
- return clickButton(parentSelectors, 'reloadPageBtn');
26
+ return clickButton(parentSelectors, 'reloadBtn');
27
27
  }
28
28
  export function clickCloseButton(parentSelectors) {
29
29
  return clickButton(parentSelectors, 'closeBtn');
@@ -40,6 +40,9 @@ export function clickYesButton(parentSelectors) {
40
40
  export function clickNoButton(parentSelectors) {
41
41
  return clickButton(parentSelectors, 'noBtn');
42
42
  }
43
+ export function clickExpandButton(parentSelectors) {
44
+ return clickButton(parentSelectors, 'expandBtn');
45
+ }
43
46
  export function clickXButton(parentSelectors) {
44
47
  return clickButton(parentSelectors, 'xBtn');
45
48
  }
@@ -25,6 +25,21 @@ import {
25
25
  getModelFromGridSelector,
26
26
  getGridRowSelectorById,
27
27
  } from './grid_functions.js';
28
+ import {
29
+ hasNodeWithFieldValue,
30
+ getNodeWithFieldValue,
31
+ selectTreeNodeById,
32
+ selectTreeNodeIfNotAlreadySelectedById,
33
+ verifyTreeRecordDoesNotExistByValue,
34
+ verifyTreeRecordExistsByValue,
35
+ verifyTreeRecordExistsById,
36
+ verifyTreeRecordDoesNotExistById,
37
+ verifyTreeNodeIsSelectedById,
38
+ getModelFromTreeName,
39
+ getModelFromTreeSelector,
40
+ getTreeNodeSelectorById,
41
+ getFirstTreeRootNode,
42
+ } from './tree_functions.js';
28
43
  import {
29
44
  verifyNoErrorBox,
30
45
  } from './common_functions.js';
@@ -59,6 +74,7 @@ import _ from 'lodash';
59
74
  const $ = Cypress.$;
60
75
 
61
76
 
77
+ // Form fields
62
78
  export function crudCombo(selector, newData, editData, schema, ancillaryData, level = 0) {
63
79
  cy.then(() => {
64
80
  Cypress.log({ name: 'crudCombo' });
@@ -92,28 +108,33 @@ export function crudTag(selector, newData, editData, schema, ancillaryData, leve
92
108
 
93
109
  clickTrigger(selector);
94
110
  }
95
- export function crudSideGridRecord(gridSelector, newData, editData, schema, ancillaryData, level = 0) {
96
- // NOTE: the 'level' arg allows this fn to be called recursively
97
- // and to use the @id alias correctly, keeping track of the level of recursion
98
- // so the CRUD operations don't step on each other at different levels.
111
+
112
+
113
+ // Grid
114
+ export function crudWindowedGridRecord(gridSelector, newData, editData, schema, ancillaryData, level = 0) {
115
+
99
116
  cy.then(() => {
100
- Cypress.log({ name: 'crudSideGridRecord ' + gridSelector });
101
- });
102
-
117
+ Cypress.log({ name: 'crudWindowedGridRecord ' + gridSelector });
118
+ });
119
+
103
120
  getDomNode(gridSelector).scrollIntoView();
104
121
 
105
122
  // add
106
- addGridRecord(gridSelector, newData, schema, ancillaryData, level); // saves the id in @id
107
-
123
+ addWindowedGridRecord(gridSelector, newData, schema, ancillaryData, level); // saves the id in @id
124
+
108
125
  cy.get('@id' + level).then((id) => {
109
126
 
127
+ cy.then(() => {
128
+ Cypress.log({ name: 'crudWindowedGridRecord: continue thru CRUD ' + gridSelector });
129
+ });
130
+
110
131
  // read
111
132
  clickReloadButton(gridSelector);
112
133
  cy.wait(1000); // allow time for grid to load
113
134
  verifyGridRecordExistsById(gridSelector, id);
114
135
 
115
136
  // edit
116
- editGridRecord(gridSelector, editData, schema, id);
137
+ editWindowedGridRecord(gridSelector, editData, schema, id);
117
138
 
118
139
  // delete
119
140
  verifyGridRecordExistsById(gridSelector, id);
@@ -121,16 +142,16 @@ export function crudSideGridRecord(gridSelector, newData, editData, schema, anci
121
142
  verifyGridRecordDoesNotExistById(gridSelector, id);
122
143
  });
123
144
  }
124
- export function crudWindowedGridRecord(gridSelector, newData, editData, schema, ancillaryData, level = 0) {
145
+ export function crudInlineGridRecord(gridSelector, newData, editData, schema, ancillaryData, level = 0) {
125
146
 
126
147
  cy.then(() => {
127
- Cypress.log({ name: 'crudWindowedGridRecord ' + gridSelector });
128
- });
148
+ Cypress.log({ name: 'crudInlineGridRecord ' + gridSelector });
149
+ });
129
150
 
130
151
  getDomNode(gridSelector).scrollIntoView();
131
152
 
132
153
  // add
133
- addWindowedGridRecord(gridSelector, newData, schema, ancillaryData, level); // saves the id in @id
154
+ addInlineGridRecord(gridSelector, newData, schema, ancillaryData, level); // saves the id in @id
134
155
 
135
156
  cy.get('@id' + level).then((id) => {
136
157
 
@@ -144,7 +165,7 @@ export function crudWindowedGridRecord(gridSelector, newData, editData, schema,
144
165
  verifyGridRecordExistsById(gridSelector, id);
145
166
 
146
167
  // edit
147
- editWindowedGridRecord(gridSelector, editData, schema, id);
168
+ editInlineGridRecord(gridSelector, editData, schema, id);
148
169
 
149
170
  // delete
150
171
  verifyGridRecordExistsById(gridSelector, id);
@@ -152,22 +173,20 @@ export function crudWindowedGridRecord(gridSelector, newData, editData, schema,
152
173
  verifyGridRecordDoesNotExistById(gridSelector, id);
153
174
  });
154
175
  }
155
- export function crudInlineGridRecord(gridSelector, newData, editData, schema, ancillaryData, level = 0) {
156
-
176
+ export function crudSideGridRecord(gridSelector, newData, editData, schema, ancillaryData, level = 0) {
177
+ // NOTE: the 'level' arg allows this fn to be called recursively
178
+ // and to use the @id alias correctly, keeping track of the level of recursion
179
+ // so the CRUD operations don't step on each other at different levels.
157
180
  cy.then(() => {
158
- Cypress.log({ name: 'crudInlineGridRecord ' + gridSelector });
159
- });
160
-
181
+ Cypress.log({ name: 'crudSideGridRecord ' + gridSelector });
182
+ });
183
+
161
184
  getDomNode(gridSelector).scrollIntoView();
162
185
 
163
186
  // add
164
- addInlineGridRecord(gridSelector, newData, schema, ancillaryData, level); // saves the id in @id
165
-
166
- cy.get('@id' + level).then((id) => {
187
+ addGridRecord(gridSelector, newData, schema, ancillaryData, level); // saves the id in @id
167
188
 
168
- cy.then(() => {
169
- Cypress.log({ name: 'crudWindowedGridRecord: continue thru CRUD ' + gridSelector });
170
- });
189
+ cy.get('@id' + level).then((id) => {
171
190
 
172
191
  // read
173
192
  clickReloadButton(gridSelector);
@@ -175,7 +194,7 @@ export function crudInlineGridRecord(gridSelector, newData, editData, schema, an
175
194
  verifyGridRecordExistsById(gridSelector, id);
176
195
 
177
196
  // edit
178
- editInlineGridRecord(gridSelector, editData, schema, id);
197
+ editGridRecord(gridSelector, editData, schema, id);
179
198
 
180
199
  // delete
181
200
  verifyGridRecordExistsById(gridSelector, id);
@@ -384,7 +403,221 @@ export function switchToViewModeIfNecessary(editorSelector) {
384
403
  }
385
404
 
386
405
 
387
- // Manager screen CRUD functions
406
+ // Tree
407
+ export function crudWindowedTreeRecord(treeSelector, newData, editData, schema, ancillaryData, level = 0) {
408
+
409
+ cy.then(() => {
410
+ Cypress.log({ name: 'crudWindowedTreeRecord ' + treeSelector });
411
+ });
412
+
413
+ getDomNode(treeSelector).scrollIntoView();
414
+
415
+ // add
416
+ addWindowedTreeRecord(treeSelector, newData, schema, ancillaryData, level); // saves the id in @id
417
+
418
+ cy.get('@id' + level).then((id) => {
419
+
420
+ cy.then(() => {
421
+ Cypress.log({ name: 'crudWindowedTreeRecord: continue thru CRUD ' + treeSelector });
422
+ });
423
+
424
+ // read
425
+ clickReloadButton(treeSelector);
426
+ cy.wait(1000); // allow time for tree to load
427
+ verifyTreeRecordExistsById(treeSelector, id);
428
+
429
+ // edit
430
+ editWindowedTreeRecord(treeSelector, editData, schema, id);
431
+
432
+ // delete
433
+ verifyTreeRecordExistsById(treeSelector, id);
434
+ deleteTreeRecord(treeSelector, id);
435
+ verifyTreeRecordDoesNotExistById(treeSelector, id);
436
+ });
437
+ }
438
+ export function crudSideTreeRecord(treeSelector, newData, editData, schema, ancillaryData, level = 0) {
439
+ // NOTE: the 'level' arg allows this fn to be called recursively
440
+ // and to use the @id alias correctly, keeping track of the level of recursion
441
+ // so the CRUD operations don't step on each other at different levels.
442
+ cy.then(() => {
443
+ Cypress.log({ name: 'crudSideTreeRecord ' + treeSelector });
444
+ });
445
+
446
+ getDomNode(treeSelector).scrollIntoView();
447
+
448
+ // add
449
+ addTreeRecord(treeSelector, newData, schema, ancillaryData, level); // saves the id in @id
450
+
451
+ cy.get('@id' + level).then((id) => {
452
+
453
+ // read
454
+ clickReloadButton(treeSelector);
455
+ cy.wait(1000); // allow time for tree to load
456
+ verifyTreeRecordExistsById(treeSelector, id);
457
+
458
+ // edit
459
+ editTreeRecord(treeSelector, editData, schema, id);
460
+
461
+ // delete
462
+ verifyTreeRecordExistsById(treeSelector, id);
463
+ deleteTreeRecord(treeSelector, id);
464
+ verifyTreeRecordDoesNotExistById(treeSelector, id);
465
+ });
466
+ }
467
+ export function addTreeRecord(treeSelector, fieldValues, schema, ancillaryData, level = 0) {
468
+
469
+ cy.then(() => {
470
+ Cypress.log({ name: 'addTreeRecord ' + treeSelector });
471
+ });
472
+
473
+ const
474
+ editorSelector = treeSelector + '/editor',
475
+ viewerSelector = editorSelector + '/viewer',
476
+ formSelector = editorSelector + '/form';
477
+
478
+ // BEGIN MOD
479
+ // select the root node
480
+ getFirstTreeRootNode(treeSelector).then ((rootNode) => {
481
+
482
+ // get the rootNodeId
483
+ const id = rootNode.attr('data-testid').split('-')[1];
484
+ selectTreeNodeIfNotAlreadySelectedById(treeSelector, id)
485
+ });
486
+ // END MOD
487
+
488
+
489
+ clickAddButton(treeSelector);
490
+ getDomNode(formSelector).should('exist');
491
+
492
+ fillForm(formSelector, fieldValues, schema, level +1);
493
+ cy.wait(500); // allow validator to enable save button
494
+ // TODO: Change this to wait until save button is enabled
495
+
496
+ let method = 'add';
497
+ if (schema.repository.isRemotePhantomMode) {
498
+ method = 'edit';
499
+ }
500
+ cy.intercept('POST', '**/' + method + '**').as('waiter');
501
+ clickSaveButton(formSelector); // it's labeled 'Add' in the form, but is really the save button
502
+ cy.wait('@waiter');
503
+
504
+ verifyNoErrorBox();
505
+
506
+ cy.wait(1000); // allow temp id to be replaced by real one
507
+
508
+ // Get and save id of new record
509
+ getDomNode([treeSelector, 'node-selected']).then((row) => {
510
+ const parent = row[0].parentNode;
511
+ cy.wrap(parent).invoke('attr', 'data-testid').then((testId) => {
512
+ const id = testId.split('-')[1];
513
+ cy.wrap(id).as('id' + level);
514
+ });
515
+ });
516
+
517
+ if (!_.isEmpty(ancillaryData)) {
518
+ _.each(ancillaryData, (data) => {
519
+ const
520
+ model = data.model,
521
+ Models = fixInflector(Inflector.camelize(Inflector.pluralize(model))),
522
+ gridType = data.gridType,
523
+ schema = data.schema,
524
+ newData = data.newData,
525
+ editData = data.editData,
526
+ ancillaryData = data.ancillaryData,
527
+ ancillaryGridSelector = formSelector + '/' + (gridType || Models + 'GridEditor');
528
+ crudWindowedGridRecord(ancillaryGridSelector, newData, editData, schema, ancillaryData, level+1);
529
+ });
530
+ }
531
+ }
532
+ export function addWindowedTreeRecord(treeSelector, fieldValues, schema, ancillaryData, level = 0) {
533
+ // adds the record as normal, then closes the editor window
534
+
535
+ cy.then(() => {
536
+ Cypress.log({ name: 'addWindowedTreeRecord ' + treeSelector });
537
+ });
538
+
539
+ addTreeRecord(treeSelector, fieldValues, schema, ancillaryData, level);
540
+
541
+ cy.then(() => {
542
+ Cypress.log({ name: 'addWindowedTreeRecord: close window ' + treeSelector });
543
+ });
544
+ const formSelector = treeSelector + '/editor/form';
545
+ clickCloseButton(formSelector);
546
+ cy.wait(500); // allow window to close
547
+ // TODO: Change this to wait until window is closed
548
+ }
549
+ export function editTreeRecord(treeSelector, fieldValues, schema, id, level = 0) {
550
+
551
+ cy.then(() => {
552
+ Cypress.log({ name: 'editTreeRecord ' + treeSelector + ' ' + id});
553
+ });
554
+
555
+ selectTreeNodeIfNotAlreadySelectedById(treeSelector, id);
556
+
557
+ const
558
+ editorSelector = treeSelector + '/editor',
559
+ viewerSelector = editorSelector + '/viewer',
560
+ formSelector = editorSelector + '/form';
561
+
562
+ const treeName = getLastPartOfPath(treeSelector);
563
+ if (treeName.match(/SideTree/)) { // as opposed to 'SideA' -- we want the side editor, not particular sides of another editor
564
+ // side editor
565
+ // switch to Edit mode if necessary
566
+ clickToEditButtonIfExists(viewerSelector);
567
+ } else {
568
+ // windowed or inline editor
569
+ clickEditButton(treeSelector);
570
+ }
571
+ cy.wait(1500); // allow form to build
572
+ getDomNode(formSelector).should('exist');
573
+
574
+ fillForm(formSelector, fieldValues, schema, level +1);
575
+ cy.wait(500); // allow validator to enable save button
576
+ // TODO: Change this to wait until save button is enabled
577
+
578
+ cy.intercept('POST', '**/edit**').as('waiter');
579
+ clickSaveButton(formSelector);
580
+ cy.wait('@waiter');
581
+
582
+ verifyNoErrorBox();
583
+ // cy.wait(1000);
584
+
585
+ }
586
+ export function editWindowedTreeRecord(treeSelector, fieldValues, schema, id, level = 0) {
587
+ // edits the record as normal, then closes the editor window
588
+
589
+ cy.then(() => {
590
+ Cypress.log({ name: 'editWindowedTreeRecord ' + treeSelector + ' ' + id});
591
+ });
592
+
593
+ editTreeRecord(treeSelector, fieldValues, schema, id, level);
594
+
595
+ const formSelector = treeSelector + '/editor/form';
596
+ clickCloseButton(formSelector);
597
+ cy.wait(500); // allow window to close
598
+ // TODO: Change this to wait until window is closed
599
+ }
600
+ export function deleteTreeRecord(treeSelector, id) {
601
+
602
+ cy.then(() => {
603
+ Cypress.log({ name: 'deleteTreeRecord ' + treeSelector + ' ' + id });
604
+ });
605
+
606
+ selectTreeNodeIfNotAlreadySelectedById(treeSelector, id);
607
+ clickDeleteButton(treeSelector);
608
+ cy.wait(500); // allow confirmation box to appear
609
+
610
+ // Click OK on confirmation box
611
+ cy.intercept('POST', '**/delete**').as('waiter');
612
+ clickYesButton('AlertDialog');
613
+ cy.wait('@waiter');
614
+
615
+ verifyNoErrorBox();
616
+ // cy.wait(1000);
617
+ }
618
+
619
+
620
+ // Manager screen
388
621
  export function runClosureTreeControlledManagerScreenCrudTests(model, schema, newData, editData) {
389
622
 
390
623
  const
@@ -474,11 +707,11 @@ export function runClosureTreeControlledManagerScreenCrudTests(model, schema, ne
474
707
  });
475
708
 
476
709
  }
477
- export function runClosureTreeManagerScreenCrudTests(model, schema, newData, editData) {
710
+ export function runClosureTreeManagerScreenCrudTests(model, schema, newData, editData, ancillaryData) {
478
711
 
479
712
  const
480
- Models = Inflector.camelize(Inflector.pluralize(model)),
481
- url = '/' + Inflector.dasherize(Inflector.underscore(Models));
713
+ Models = fixInflector(Inflector.camelize(Inflector.pluralize(model))),
714
+ url = '/' + fixInflector(Inflector.dasherize(Inflector.underscore(Models)));
482
715
 
483
716
  describe(Models + 'Manager', () => {
484
717
 
@@ -492,44 +725,34 @@ export function runClosureTreeManagerScreenCrudTests(model, schema, newData, edi
492
725
  });
493
726
  });
494
727
 
495
- afterEach(function () {
496
- cy.saveLocalStorage();
497
- logout();
498
- });
499
-
500
- it('CRUD ClosureTree', function() {
501
-
502
- const treeSelector = '/' + Models + 'TreeEditor';
728
+ // afterEach(function () {
729
+ // cy.saveLocalStorage();
730
+ // logout();
731
+ // });
503
732
 
504
- toFullMode();
505
- cy.wait(500); // wait for tree to load
733
+ it('CRUD in full mode', function() {
506
734
 
735
+ const
736
+ managerSelector = '/' + Models + 'Manager',
737
+ treeSelector = '/' + Models + 'FilteredTreeEditor';
507
738
 
508
- // TODO: Customize these for crudding a ClosureTree
739
+ toFullMode(managerSelector);
740
+ cy.wait(500); // wait for grid to load
509
741
 
742
+ crudWindowedTreeRecord(treeSelector, newData, editData, schema, ancillaryData);
510
743
 
744
+ });
511
745
 
512
-
746
+ it('CRUD in side mode', function() {
513
747
 
514
- // // add
515
- // addWindowedGridRecord(treeSelector, newData, schema); // saves the id in @id
516
-
517
- // // cy.wrap(39).as('id');
518
- // cy.get('@id').then((id) => {
748
+ const
749
+ managerSelector = '/' + Models + 'Manager',
750
+ treeSelector = '/' + Models + 'FilteredSideTreeEditor';
519
751
 
520
- // // read
521
- // clickReloadButton(treeSelector);
522
- // cy.wait(1000); // allow time for tree to load
523
- // verifyGridRecordExistsById(treeSelector, id);
752
+ toSideMode(managerSelector);
753
+ cy.wait(1000); // wait for grid to load
524
754
 
525
- // // edit
526
- // editWindowedGridRecord(treeSelector, editData, schema, id);
527
-
528
- // // delete
529
- // verifyGridRecordExistsById(treeSelector, id);
530
- // deleteGridRecord(treeSelector, id);
531
- // verifyGridRecordDoesNotExistById(treeSelector, id);
532
- // });
755
+ crudSideTreeRecord(treeSelector, newData, editData, schema, ancillaryData);
533
756
 
534
757
  });
535
758
 
@@ -0,0 +1,254 @@
1
+ import {
2
+ fixInflector,
3
+ getLastPartOfPath,
4
+ } from './utilities.js';
5
+ import {
6
+ getDomNode,
7
+ getDomNodes,
8
+ } from './dom_functions.js';
9
+ import Inflector from 'inflector-js';
10
+ import _ from 'lodash';
11
+ const $ = Cypress.$;
12
+
13
+
14
+
15
+ // Get rows
16
+ export function hasNodeWithFieldValue(treeSelector, field, value) {
17
+ return getDomNodes([treeSelector, 'row', 'cell-' + field]).contains(value);
18
+ }
19
+ export function getNodeWithFieldValue(treeSelector, field, value) {
20
+ return getDomNodes([treeSelector, 'row', 'cell-' + field]).contains(value).then((cells) => {
21
+ if (!cells.length) {
22
+ return null;
23
+ }
24
+
25
+ const cell = cells[0];
26
+ return $(cell).closest('[data-testid="row"]')[0];
27
+ });
28
+ }
29
+ export function getFirstTreeRootNode(treeSelector) {
30
+ return cy.get('[data-testid="' + treeSelector + '"]:first ' +
31
+ '[data-testid="ScrollView"]:first > div > div:first'); // this is fragile!
32
+ }
33
+ // export function getNodeWithText(tree, text) {
34
+ // return getNodes(tree).contains(text);
35
+ // }
36
+ // export function getNodeWithId(tree, id) {
37
+ // return getNodes(tree, '[data-cy-recordid=' + id + ']');
38
+ // }
39
+ // export function getNodeWithIx(tree, ix) {
40
+ // return getNodes(tree, '[data-recordindex=' + ix + ']');
41
+ // }
42
+
43
+
44
+ // Select rows
45
+ export function selectTreeNodeById(treeSelector, id) {
46
+ cy.then(() => {
47
+ Cypress.log({ name: 'selectTreeNodeById ' + treeSelector + ' ' + id});
48
+ });
49
+ const rowSelector = getTreeNodeSelectorById(treeSelector, id);
50
+ getDomNode([treeSelector, rowSelector])
51
+ .click();
52
+ }
53
+ export function selectTreeNodeIfNotAlreadySelectedById(treeSelector, id) {
54
+ cy.then(() => {
55
+ Cypress.log({ name: 'selectTreeNodeIfNotAlreadySelectedById ' + treeSelector + ' ' + id});
56
+ });
57
+ const rowSelector = getTreeNodeSelectorById(treeSelector, id);
58
+ getDomNode([treeSelector, rowSelector]).then((row) => {
59
+ const found = row.find('[data-testid="node-selected"]')
60
+ if (!found.length) {
61
+ selectTreeNodeById(treeSelector, id);
62
+ }
63
+ })
64
+ }
65
+ // export function selectNodeWithText(tree, text) {
66
+ // getNodeWithText(tree, text).click(5, 5);
67
+ // }
68
+ // export function selectNodeWithIx(tree, ix) {
69
+ // getNodeWithIx(tree, ix).click(5, 5);
70
+ // }
71
+ // export function cmdClickNodeWithId(tree, id) {
72
+ // getNodeWithId(tree, id).click('left', { metaKey: true });
73
+ // }
74
+
75
+
76
+ // // Double-click rows
77
+ // export function doubleClickNodeWithText(tree, text) {
78
+ // getNodeWithText(tree, text).dblclick();
79
+ // }
80
+ // export function doubleClickNodeWithId(tree, id) {
81
+ // getNodeWithId(tree, id).dblclick();
82
+ // }
83
+ // export function doubleClickNodeWithIx(tree, ix) {
84
+ // getNodeWithIx(tree, ix).dblclick();
85
+ // }
86
+
87
+
88
+ export function verifyTreeRecordDoesNotExistByValue(treeSelector, fieldValues, schema) {
89
+ const
90
+ field = schema.model.displayProperty,
91
+ value = fieldValues[field];
92
+
93
+ getDomNodes([treeSelector, 'row', 'cell-' + field])
94
+ .contains(value, { timeout: 500 })
95
+ .should('not.exist');
96
+ }
97
+ export function verifyTreeRecordExistsByValue(treeSelector, fieldValues, schema) {
98
+ const
99
+ field = schema.model.displayProperty,
100
+ value = fieldValues[field];
101
+
102
+ getDomNodes([treeSelector, 'row', 'cell-' + field])
103
+ .contains(value, { timeout: 500 })
104
+ .should('exist');
105
+ }
106
+ export function verifyTreeRecordExistsById(treeSelector, id) {
107
+ cy.then(() => {
108
+ Cypress.log({ name: 'verifyTreeRecordExistsById ' + treeSelector + ' ' + id });
109
+ });
110
+
111
+ const rowSelector = getTreeNodeSelectorById(treeSelector, id);
112
+ getDomNodes([treeSelector, rowSelector])
113
+ .should('exist');
114
+ }
115
+ export function verifyTreeRecordDoesNotExistById(treeSelector, id) {
116
+ cy.then(() => {
117
+ Cypress.log({ name: 'verifyTreeRecordDoesNotExistById ' + treeSelector + ' ' + id });
118
+ });
119
+ const rowSelector = getTreeNodeSelectorById(treeSelector, id);
120
+ getDomNodes([treeSelector, rowSelector])
121
+ .should('not.exist');
122
+ }
123
+ export function verifyTreeNodeIsSelectedById(treeSelector, id) {
124
+ cy.then(() => {
125
+ Cypress.log({ name: 'verifyTreeNodeIsSelectedById ' + treeSelector + ' ' + id});
126
+ });
127
+ const rowSelector = getTreeNodeSelectorById(treeSelector, id);
128
+ getDomNodes([treeSelector, rowSelector, 'node-selected'])
129
+ .should('exist');
130
+ }
131
+
132
+
133
+
134
+
135
+
136
+ // export function addRecordWithInlineEditor(tree, fieldValues, schema) {
137
+ // clickTreeAddButton(tree, true);
138
+ // fillInlineForm(tree, fieldValues, schema);
139
+ // cy.route('POST', '**/extAdd**').as('addWaiter');
140
+ // submitInlineForm(tree);
141
+ // cy.wait('@addWaiter');
142
+ // verifyNoErrorBox();
143
+ // getSelectedNodeId(tree); // Adds @id alias
144
+ // cy.wait(1000);
145
+ // }
146
+ // export function addRecordWithWindowedEditor(tree, editorCls, fieldValues, schema) {
147
+ // clickTreeAddButton(tree, true);
148
+ // fillWindowedForm(editorCls, fieldValues, schema);
149
+ // cy.route('POST', '**/extAdd**').as('addWaiter');
150
+ // submitWindowedForm(editorCls);
151
+ // cy.wait('@addWaiter');
152
+ // verifyNoErrorBox();
153
+ // getSelectedNodeId(tree); // Adds @id alias
154
+ // cy.wait(1000);
155
+ // }
156
+ // export function editRecordWithInlineEditor(tree, fieldValues, schema) {
157
+ // cy.get("@id").then((id) => {
158
+ // doubleClickNodeWithId(tree, id);
159
+ // fillInlineForm(tree, fieldValues, schema);
160
+ // cy.route('POST', '**/extEdit**').as('editWaiter');
161
+ // submitInlineForm(tree);
162
+ // cy.wait('@editWaiter');
163
+ // verifyNoErrorBox();
164
+ // cy.wait(500);
165
+ // });
166
+ // }
167
+ // export function editRecordWithWindowedEditor(tree, editorCls, fieldValues, schema) {
168
+ // cy.get("@id").then((id) => {
169
+ // doubleClickNodeWithId(tree, id);
170
+ // fillWindowedForm(editorCls, fieldValues, schema);
171
+ // cy.route('POST', '**/extEdit**').as('editWaiter');
172
+ // submitWindowedForm(editorCls);
173
+ // cy.wait('@editWaiter');
174
+ // verifyNoErrorBox();
175
+ // });
176
+ // }
177
+ // export function removeRecord(tree) {
178
+ // cy.get("@id").then((id) => {
179
+ // cy.wait(500);
180
+ // clickTreeRemoveButton(tree);
181
+ // cy.route('POST', '**/extDelete**').as('removeWaiter');
182
+ // clickMessageBoxDefaultButton();
183
+ // cy.wait('@removeWaiter');
184
+ // verifyNoErrorBox();
185
+ // });
186
+ // }
187
+
188
+
189
+ // Tree Utilities
190
+ export function getModelFromTreeName(treeName) {
191
+ // try to match with something like 'EquipmentFilteredTreeEditor'
192
+ const
193
+ pattern = '^' + // start
194
+ '([\\w]+?)' + // model name
195
+ '(?=Filtered|Inline|Side|Tree)' + // positive lookahead to guarantee one of these is the next word
196
+ '(Filtered)?' + // optional
197
+ '(Inline)?' + // optional
198
+ '(Side)?' + // optional
199
+ 'Tree' + // required
200
+ '(Editor)?' + // optional
201
+ '(Side[AB])?' + // optional
202
+ '$', // end
203
+ regex = new RegExp(pattern),
204
+ match = treeName.match(regex);
205
+ return match?.length ? match[1] : null;
206
+ }
207
+ export function getModelFromTreePath(treePath) {
208
+ // try to match with something like '...__eq_manufacturer_id/tree'
209
+ let
210
+ pattern = '^' + // start
211
+ '.*' + // previous selector path (ignore)
212
+ '(?=__)' + // positive lookahead to guarantee '__' is the next word
213
+ '__' + // required
214
+ '([A-Za-z_]+)' + // field name (model name underscored, singularized)
215
+ '([\\d]+)?' + // optional (digit, e.g. eq_engine_model1_id)
216
+ '_id/(combo/)?tree' + // required
217
+ '$', // end
218
+ regex = new RegExp(pattern),
219
+ match = treePath.match(regex);
220
+ if (!match) {
221
+ // try to match with something like '.../work_orders__equipment/combo/tree"'
222
+ pattern = '^' + // start
223
+ '.*' + // previous selector path (ignore)
224
+ '(?=__)' + // positive lookahead to guarantee '__' is the next word
225
+ '__' + // required
226
+ '([A-Za-z_]+)' + // field name (model name underscored, pluralized)
227
+ '/(combo/)?tree' + // required
228
+ '$', // end
229
+ regex = new RegExp(pattern);
230
+ match = treePath.match(regex);
231
+ }
232
+ return match?.length ? match[1] : null;
233
+ }
234
+ export function getModelFromTreeSelector(treeSelector) {
235
+ const treeName = getLastPartOfPath(treeSelector);
236
+ let model = getModelFromTreeName(treeName);
237
+ if (!model) {
238
+ model = getModelFromTreePath(treeSelector);
239
+ if (model) {
240
+ model = fixInflector(Inflector.camelize(Inflector.pluralize(model)));
241
+ }
242
+ }
243
+ return model;
244
+ }
245
+ export function getTreeNodeSelectorById(treeSelector, id) {
246
+ const
247
+ model = getModelFromTreeSelector(treeSelector);
248
+
249
+ if (!model) {
250
+ debugger;
251
+ }
252
+ const inflected = fixInflector(Inflector.camelize(Inflector.pluralize(model)));
253
+ return inflected + '-' + id;
254
+ }