@onehat/ui 0.4.108 → 0.4.111

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.4.108",
3
+ "version": "0.4.111",
4
4
  "description": "Base UI for OneHat apps",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -320,7 +320,7 @@ function TagComponent(props) {
320
320
  />;
321
321
  }
322
322
  switch (items.length) {
323
- case 1: height = 250; break;
323
+ case 1: height = 300; break;
324
324
  case 2: height = 400; break;
325
325
  default: height = 600; break;
326
326
  }
@@ -321,7 +321,11 @@ function Form(props) {
321
321
  type = 'Text';
322
322
  }
323
323
  }
324
- const isCombo = type?.match && type.match(/Combo/);
324
+ const
325
+ Element = getComponentFromType(type),
326
+ shouldHideFieldUi = type === 'Hidden',
327
+ isCombo = type?.match && type.match(/Combo/);
328
+
325
329
  if (config.hasOwnProperty('autoLoad')) {
326
330
  editorTypeProps.autoLoad = config.autoLoad;
327
331
  } else {
@@ -335,9 +339,8 @@ function Form(props) {
335
339
  editorTypeProps.showXButton = true;
336
340
  }
337
341
  }
338
- const Element = getComponentFromType(type);
339
342
 
340
- if (isEditorViewOnly || !isEditable) {
343
+ if ((isEditorViewOnly || !isEditable) && !shouldHideFieldUi) {
341
344
  let value = null;
342
345
  if (renderer) {
343
346
  value = renderer(record);
@@ -483,6 +486,9 @@ function Form(props) {
483
486
  {...dynamicProps}
484
487
  className={elementClassName}
485
488
  />;
489
+ if (shouldHideFieldUi) {
490
+ return element;
491
+ }
486
492
 
487
493
  const dirtyIcon = isDirty && !disableDirtyIcon ?
488
494
  <Icon
@@ -532,6 +538,7 @@ function Form(props) {
532
538
  isEditable = true,
533
539
  isEditingEnabledInPlainEditor,
534
540
  label,
541
+ disableLabel = false,
535
542
  labelWidth,
536
543
  items,
537
544
  onChange: onEditorChange,
@@ -588,7 +595,11 @@ function Form(props) {
588
595
  type = 'Text';
589
596
  }
590
597
  }
591
- const isCombo = type?.match && type.match(/Combo/);
598
+ const
599
+ Element = getComponentFromType(type),
600
+ shouldHideFieldUi = type === 'Hidden',
601
+ isCombo = type?.match && type.match(/Combo/);
602
+
592
603
  if (item.hasOwnProperty('autoLoad')) {
593
604
  editorTypeProps.autoLoad = item.autoLoad;
594
605
  } else {
@@ -602,7 +613,6 @@ function Form(props) {
602
613
  editorTypeProps.showXButton = true;
603
614
  }
604
615
  }
605
- const Element = getComponentFromType(type);
606
616
 
607
617
  if (inArray(type, ['Column', 'Row', 'FieldSet'])) {
608
618
  if (_.isEmpty(items)) {
@@ -676,7 +686,7 @@ function Form(props) {
676
686
  label = propertyDef.title;
677
687
  }
678
688
 
679
- if (isEditorViewOnly || !isEditable) {
689
+ if ((isEditorViewOnly || !isEditable) && !shouldHideFieldUi) {
680
690
  let value = null;
681
691
  if (isSingle) {
682
692
  value = record?.properties?.[name]?.displayValue || null;
@@ -713,7 +723,10 @@ function Form(props) {
713
723
  {...viewerTypeProps}
714
724
  className={elementClassName}
715
725
  />;
716
- if (!disableLabels && label) {
726
+ if (shouldHideFieldUi) {
727
+ return null;
728
+ }
729
+ if (!disableLabels && !disableLabel && label) {
717
730
  const style = {};
718
731
  if (defaults?.labelWidth) {
719
732
  style.width = defaults.labelWidth;
@@ -846,6 +859,9 @@ function Form(props) {
846
859
  {...dynamicProps}
847
860
  className={elementClassName}
848
861
  />;
862
+ if (shouldHideFieldUi) {
863
+ return element;
864
+ }
849
865
  let message = null;
850
866
  if (error) {
851
867
  message = error.message;
@@ -905,7 +921,7 @@ function Form(props) {
905
921
  }
906
922
  }
907
923
  const labelToUse = dynamicProps.label || label;
908
- if (!disableLabels && labelToUse && editorType !== EDITOR_TYPE__INLINE) {
924
+ if (!disableLabels && !disableLabel && labelToUse && editorType !== EDITOR_TYPE__INLINE) {
909
925
  const style = {};
910
926
  if (defaults?.labelWidth) {
911
927
  style.width = defaults.labelWidth;
@@ -933,7 +949,7 @@ function Form(props) {
933
949
  {element}
934
950
  </VStack>;
935
951
  }
936
- } else if (disableLabels && requiredIndicator) {
952
+ } else if ((disableLabels || disableLabel) && requiredIndicator) {
937
953
  element = <HStack className="Form-HStack10 w-full">
938
954
  {requiredIndicator}
939
955
  {element}
@@ -1274,7 +1290,10 @@ function Form(props) {
1274
1290
  showCancelBtn = false,
1275
1291
  showSaveBtn = false,
1276
1292
  showSubmitBtn = false,
1277
- isAddMode = getEditorMode() === EDITOR_MODE__ADD;
1293
+ isAddMode = getEditorMode() === EDITOR_MODE__ADD,
1294
+ isEditableMode =
1295
+ getEditorMode() === EDITOR_MODE__ADD ||
1296
+ getEditorMode() === EDITOR_MODE__EDIT;
1278
1297
  if (containerWidth) { // we need to render this component twice in order to get the container width. Skip this on first render
1279
1298
 
1280
1299
  // create editor
@@ -1363,7 +1382,7 @@ function Form(props) {
1363
1382
  if (onDelete && getEditorMode() === EDITOR_MODE__EDIT && isSingle) {
1364
1383
  showDeleteBtn = true;
1365
1384
  }
1366
- if (!isEditorViewOnly && !hideResetButton) {
1385
+ if (!isEditorViewOnly && isEditableMode && !hideResetButton) {
1367
1386
  showResetBtn = true;
1368
1387
  }
1369
1388
  // determine whether we should show the close or cancel button
@@ -1394,7 +1413,7 @@ function Form(props) {
1394
1413
  }
1395
1414
  }
1396
1415
  }
1397
- if (!isEditorViewOnly && onSave) {
1416
+ if (!isEditorViewOnly && isEditableMode && onSave) {
1398
1417
  showSaveBtn = true;
1399
1418
  }
1400
1419
  if (!!onSubmit) {
@@ -62,9 +62,9 @@ export default function withPdfButtons(WrappedComponent) {
62
62
  buildModalItems = () => {
63
63
  // Build a cloned PDF item tree so we never mutate source items by reference.
64
64
  const
65
- itemsTouse = pdfItems || items,
65
+ itemsToUse = pdfItems || _.filter(items, (item) => item?.type !== 'Hidden'),
66
66
  ancillaryItemsToUse = pdfAncillaryItems || ancillaryItems,
67
- modalItems = _.compact(_.map(itemsTouse, (item, ix) => buildNextLayer(item, ix, columnDefaults)));
67
+ modalItems = _.compact(_.map(itemsToUse, (item, ix) => buildNextLayer(item, ix, columnDefaults)));
68
68
 
69
69
  if (!_.isEmpty(ancillaryItemsToUse)) {
70
70
  const
@@ -23,7 +23,7 @@ import _ from 'lodash';
23
23
  export function checkPermission(permission) {
24
24
  const
25
25
  reduxState = UiGlobals.redux?.getState(),
26
- permissions = reduxState?.app?.permissions || [];
26
+ permissions = reduxState?.app?.permissions || reduxState?.auth?.permissions || [];
27
27
  let hasPermission = inArray(permission, permissions);
28
28
  if (hasPermission) {
29
29
  return true;
@@ -436,6 +436,10 @@ export default function withPresetButtons(WrappedComponent) {
436
436
  showInfo('Copied to clipboard!');
437
437
  }
438
438
  },
439
+ getColumnsConfigForDownload = () => {
440
+ const activeColumnsConfig = localColumnsConfig.length ? localColumnsConfig : props.columnsConfig;
441
+ return _.filter(activeColumnsConfig, (config) => !config.isHidden);
442
+ },
439
443
  onUploadDownload = () => {
440
444
  const onUploadDecorator = async () => {
441
445
  if (onUpload) {
@@ -454,7 +458,7 @@ export default function withPresetButtons(WrappedComponent) {
454
458
  reference="uploadsDownloads"
455
459
  onClose={hideModal}
456
460
  Repository={Repository}
457
- columnsConfig={props.columnsConfig}
461
+ columnsConfig={getColumnsConfigForDownload()}
458
462
  uploadHeaders={uploadHeaders}
459
463
  uploadParams={uploadParams}
460
464
  onUpload={onUploadDecorator}
@@ -471,7 +475,7 @@ export default function withPresetButtons(WrappedComponent) {
471
475
  onClose={hideModal}
472
476
  isDownloadOnly={true}
473
477
  Repository={Repository}
474
- columnsConfig={props.columnsConfig}
478
+ columnsConfig={getColumnsConfigForDownload()}
475
479
  downloadHeaders={downloadHeaders}
476
480
  downloadParams={downloadParams}
477
481
  />,
@@ -8,8 +8,10 @@ import clsx from 'clsx';
8
8
  import { useSelector, useDispatch } from 'react-redux';
9
9
  import {
10
10
  logout,
11
- selectUser,
12
11
  } from '@src/Models/Slices/AppSlice';
12
+ import {
13
+ selectUser,
14
+ } from '../../Models/Slices/AuthSlice.js';
13
15
  import IconButton from '../Buttons/IconButton';
14
16
  import RightFromBracket from '../Icons/RightFromBracket';
15
17
  import User from '../Icons/User';
@@ -107,6 +107,14 @@ function Viewer(props) {
107
107
  }),
108
108
  isSideEditor = editorType === EDITOR_TYPE__SIDE,
109
109
  isSmartEditor = editorType === EDITOR_TYPE__SMART,
110
+ parentEditorModeRaw = (getEditorMode && getEditorMode()) || editorMode || null,
111
+ parentEditorMode = parentEditorModeRaw === EDITOR_MODE__ADD
112
+ ? EDITOR_MODE__EDIT
113
+ : parentEditorModeRaw,
114
+ normalizedParentEditorMode =
115
+ parentEditorMode === EDITOR_MODE__EDIT || parentEditorMode === EDITOR_MODE__VIEW
116
+ ? parentEditorMode
117
+ : null,
110
118
  styles = UiGlobals.styles,
111
119
  flex = props.flex || 1,
112
120
  buildFromItems = () => {
@@ -125,6 +133,7 @@ function Viewer(props) {
125
133
  title,
126
134
  name,
127
135
  label,
136
+ disableLabel = false,
128
137
  items,
129
138
  useSelectorId = false,
130
139
  isHidden = false,
@@ -160,7 +169,11 @@ function Viewer(props) {
160
169
  type = 'Text';
161
170
  }
162
171
  }
163
- const isCombo = type?.match && type.match(/Combo/);
172
+ const
173
+ Element = getComponentFromType(type),
174
+ shouldHideFieldUi = type === 'Hidden',
175
+ isCombo = type?.match && type.match(/Combo/);
176
+
164
177
  if (item.hasOwnProperty('autoLoad')) {
165
178
  viewerTypeProps.autoLoad = item.autoLoad;
166
179
  } else {
@@ -175,7 +188,6 @@ function Viewer(props) {
175
188
  if (type?.match(/(Grid|GridEditor)$/)) {
176
189
  viewerTypeProps.canEditorViewOnly = true;
177
190
  }
178
- const Element = getComponentFromType(type);
179
191
 
180
192
  if (inArray(type, ['Column', 'Row', 'FieldSet'])) {
181
193
  if (_.isEmpty(items)) {
@@ -264,6 +276,10 @@ function Viewer(props) {
264
276
  >{children}</Element>;
265
277
  }
266
278
 
279
+ if (shouldHideFieldUi) {
280
+ return null;
281
+ }
282
+
267
283
  if (!label && Repository && propertyDef?.title) {
268
284
  label = propertyDef.title;
269
285
  }
@@ -334,7 +350,7 @@ function Viewer(props) {
334
350
  </HStack>;
335
351
  }
336
352
 
337
- if (!disableLabels && label) {
353
+ if (!disableLabels && !disableLabel && label) {
338
354
  const style = {};
339
355
  if (defaults?.labelWidth) {
340
356
  style.width = defaults.labelWidth;
@@ -359,14 +375,6 @@ function Viewer(props) {
359
375
  buildAncillary = () => {
360
376
  const
361
377
  validAncillaryItems = _.filter(ancillaryItems, (item) => !!item), // filter out any null/undefined items
362
- parentEditorModeRaw = (getEditorMode && getEditorMode()) || editorMode || null,
363
- parentEditorMode = parentEditorModeRaw === EDITOR_MODE__ADD
364
- ? EDITOR_MODE__EDIT
365
- : parentEditorModeRaw,
366
- normalizedParentEditorMode =
367
- parentEditorMode === EDITOR_MODE__EDIT || parentEditorMode === EDITOR_MODE__VIEW
368
- ? parentEditorMode
369
- : null,
370
378
  components = [];
371
379
  setAncillaryButtons([]);
372
380
  if (validAncillaryItems.length) {
@@ -584,7 +592,7 @@ function Viewer(props) {
584
592
 
585
593
  <Toolbar className="justify-end">
586
594
  <HStack className="flex-1 items-center">
587
- <Text className="text-[20px] ml-1 text-grey-500">{isEditorModeControlledByParent ? 'View Mode (Inherited)' : 'View Mode'}</Text>
595
+ <Text className="text-[20px] ml-1 text-grey-500">{isEditorModeControlledByParent && normalizedParentEditorMode === EDITOR_MODE__VIEW ? 'View Mode (Inherited)' : 'View Mode'}</Text>
588
596
  </HStack>
589
597
  {onEditMode && (!canUser || canUser(EDIT)) &&
590
598
  <Button
@@ -18,6 +18,11 @@ import Upload from '../Icons/Upload';
18
18
  import Cookies from 'js-cookie';
19
19
  import _ from 'lodash';
20
20
 
21
+ const
22
+ MODES__GRID = 'grid',
23
+ MODES__BATCH = 'batch',
24
+ MODES__TEMPLATE = 'template';
25
+
21
26
  function UploadsDownloadsWindow(props) {
22
27
  const {
23
28
  Repository,
@@ -37,8 +42,8 @@ function UploadsDownloadsWindow(props) {
37
42
  showInfo,
38
43
  } = props,
39
44
  [importFile, setImportFile] = useState(null),
40
- [width, height] = useAdjustedWindowSize(400, 450),
41
- onDownload = (isTemplate = false) => {
45
+ [width, height] = useAdjustedWindowSize(400, 550),
46
+ onDownload = (mode) => {
42
47
 
43
48
  const win = window.open('');
44
49
  win.document.write('<html><head><title>Downloading</title></head><body><img style="position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);" src="' +
@@ -63,7 +68,7 @@ function UploadsDownloadsWindow(props) {
63
68
  columns,
64
69
  order,
65
70
  model,
66
- isTemplate,
71
+ mode,
67
72
  ...Repository._params,
68
73
  ...downloadParams,
69
74
  }),
@@ -79,9 +84,6 @@ function UploadsDownloadsWindow(props) {
79
84
  }
80
85
  }, 1000);
81
86
  },
82
- onDownloadTemplate = () => {
83
- onDownload(true);
84
- },
85
87
  onUploadLocal = async () => {
86
88
  const
87
89
  url = Repository.api.baseURL + Repository.name + '/uploadBatch',
@@ -130,24 +132,40 @@ function UploadsDownloadsWindow(props) {
130
132
  const items = [
131
133
  {
132
134
  type: 'DisplayField',
133
- text: 'Download an Excel file of the current grid contents.',
135
+ text: "Get the current grid's contents as an Excel file.",
136
+ },
137
+ {
138
+ type: 'Button',
139
+ text: 'Get as Excel',
140
+ isEditable: false,
141
+ icon: Excel,
142
+ _icon: {
143
+ size: 'md',
144
+ },
145
+ onPress: () => onDownload(MODES__GRID),
146
+ className: 'mb-5',
147
+ },
148
+ {
149
+ type: 'DisplayField',
150
+ text: "Batch download the current grid's records,\n"
151
+ + "so they can be edited and re-uploaded.",
134
152
  },
135
153
  {
136
154
  type: 'Button',
137
- text: 'Download',
155
+ text: 'Batch Download',
138
156
  isEditable: false,
139
157
  icon: Excel,
140
158
  _icon: {
141
159
  size: 'md',
142
160
  },
143
- onPress: () => onDownload(),
161
+ onPress: () => onDownload(MODES__BATCH),
144
162
  className: 'mb-5',
145
163
  },
146
164
  ];
147
165
  if (!isDownloadOnly) {
148
166
  items.push({
149
167
  type: 'DisplayField',
150
- text: 'Upload an Excel file to the current grid.',
168
+ text: 'Batch upload an Excel file to the current grid.',
151
169
  });
152
170
  items.push({
153
171
  type: 'File',
@@ -161,7 +179,7 @@ function UploadsDownloadsWindow(props) {
161
179
  items: [
162
180
  {
163
181
  type: 'Button',
164
- text: 'Upload',
182
+ text: 'Batch Upload',
165
183
  isEditable: false,
166
184
  icon: Upload,
167
185
  _icon: {
@@ -175,7 +193,7 @@ function UploadsDownloadsWindow(props) {
175
193
  text: 'Get Template',
176
194
  icon: Download,
177
195
  isEditable: false,
178
- onPress: onDownloadTemplate,
196
+ onPress: () => onDownload(MODES__TEMPLATE),
179
197
  },
180
198
 
181
199
  ],
@@ -0,0 +1,3 @@
1
+ export const AUTH_STATUS_UNKNOWN = 'unknown';
2
+ export const AUTH_STATUS_AUTHENTICATED = 'authenticated';
3
+ export const AUTH_STATUS_UNAUTHENTICATED = 'unauthenticated';
@@ -0,0 +1,98 @@
1
+ import UiGlobals from '../UiGlobals.js';
2
+ import oneHatData from '@onehat/data';
3
+
4
+
5
+ /**
6
+ * getTokenHeaders
7
+ *
8
+ * Main entry point for building an Authentication header object.
9
+ * If `clearAll` is true, returns a header with Authentication: null to wipe the token (e.g. on logout).
10
+ * Accepts an optional `user` arg (a plain object or @onehat/data Entity); falls back to Redux state if omitted.
11
+ *
12
+ * @param {boolean} [clearAll=false] - When true, returns a header that clears the token (Authentication: null).
13
+ * @param {object|null} [user=null] - A plain user object or @onehat/data Entity. Falls back to Redux state if null.
14
+ * @returns {object} Header object with an `Authentication` key.
15
+ */
16
+ export default function getTokenHeaders(clearAll = false, user = null) {
17
+ if (clearAll) {
18
+ return getRepositoryAuthHeaders(null);
19
+ }
20
+
21
+ const
22
+ reduxState = UiGlobals?.redux?.getState ? UiGlobals.redux.getState() : null,
23
+ scopedUser = user || reduxState?.app?.user,
24
+ token = getUserToken(scopedUser);
25
+
26
+ if (!token) {
27
+ return {};
28
+ }
29
+
30
+ return getRepositoryAuthHeaders(token);
31
+ }
32
+
33
+ /**
34
+ * getUserData
35
+ *
36
+ * Normalizes a user value into a plain data object.
37
+ * Exists because a user may arrive as either a raw plain object or a @onehat/data Entity.
38
+ *
39
+ * @param {object|null} user - A plain user object or @onehat/data Entity.
40
+ * @returns {object|null} The raw user data object, or null if no user was provided.
41
+ */
42
+ export function getUserData(user) {
43
+ if (!user) {
44
+ return null;
45
+ }
46
+ return user?.getOriginalData ? user.getOriginalData() : user;
47
+ }
48
+
49
+ /**
50
+ * getUserToken
51
+ *
52
+ * Extracts the auth token string from a user value.
53
+ * Exists because the token may be stored under either `token` or `users__token` depending on the API response shape.
54
+ * Normalizes the user via getUserData first, then returns whichever key is present, or null.
55
+ *
56
+ * @param {object|null} user - A plain user object or @onehat/data Entity.
57
+ * @returns {string|null} The token string, or null if none is found.
58
+ */
59
+ export function getUserToken(user) {
60
+ const userData = getUserData(user);
61
+ return userData?.token || userData?.users__token || null;
62
+ }
63
+
64
+ /**
65
+ * getRepositoryAuthHeaders
66
+ *
67
+ * Builds the raw header object used for authenticated API requests.
68
+ * Exists as a dedicated function so the header shape is defined in one place and can be used
69
+ * both for one-off header construction and for pushing headers to all repositories.
70
+ * Passing null (or omitting the arg) sets Authentication to null, clearing the token.
71
+ *
72
+ * @param {string|null} [token=null] - The auth token string. Pass null to clear the token.
73
+ * @returns {object} Header object with an `Authentication` key.
74
+ */
75
+ export function getRepositoryAuthHeaders(token = null) {
76
+ return {
77
+ Authentication: token ? `Token ${token}` : null,
78
+ // Cookie: null,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * setRepositoryAuthHeaders
84
+ *
85
+ * Propagates an auth token to every @onehat/data repository in one call.
86
+ * Exists to support the refresh token flow: when a new token is obtained, this immediately
87
+ * updates all repository instances and the repository globals so subsequent requests use the new token.
88
+ * Passing null clears the token everywhere (e.g. on logout).
89
+ *
90
+ * @param {string|null} [token=null] - The auth token string. Pass null to clear the token on all repositories.
91
+ * @returns {object} The header object that was applied to all repositories.
92
+ */
93
+ export function setRepositoryAuthHeaders(token = null) {
94
+ const headers = getRepositoryAuthHeaders(token);
95
+ oneHatData.setOptionsOnAllRepositories({ headers });
96
+ oneHatData.setRepositoryGlobals({ headers });
97
+ return headers;
98
+ }
@@ -1,6 +1,9 @@
1
1
  import UiGlobals from '../UiGlobals.js';
2
2
 
3
3
  export default function(clearAll = false) {
4
+
5
+ throw Error('getTokenHeaders is deprecated. Please use the new functions in authFunctions.js instead.');
6
+
4
7
  const reduxState = UiGlobals.redux.getState();
5
8
  if (!reduxState.app.user || (!reduxState.app.user.token && !reduxState.app.user.users__token)) {
6
9
  return {};
@@ -0,0 +1,37 @@
1
+ import { useEffect } from 'react';
2
+ import oneHatData from '@onehat/data';
3
+ import { CROSS_TAB_EVENT_NAME } from '@onehat/data/src/Integration/Browser/Repository/crossTabConstants.js';
4
+
5
+ /**
6
+ * Subscribes to cross-tab storage change events emitted by the Secure repository.
7
+ * When another tab writes to the Secure store, `onChange` is called with:
8
+ * { operation, key, namespacedKey, timestamp, repositoryName, repositoryType }
9
+ *
10
+ * The callback is intentionally free of Redux/app concerns — wire your own
11
+ * re-hydration logic (e.g. re-dispatch setUserThunk) in the callback.
12
+ *
13
+ * Usage:
14
+ * useCrossTabSecureSync(useCallback(async ({ key }) => {
15
+ * if (key === 'user') {
16
+ * const user = await getSecure('user');
17
+ * dispatch(setUserThunk(user));
18
+ * }
19
+ * }, [dispatch]));
20
+ *
21
+ * @param {Function} onChange - Stable callback (wrap in useCallback).
22
+ * @param {string} [repositoryName='Secure'] - Override target repository name.
23
+ */
24
+ export default function useCrossTabSecureSync(onChange, repositoryName = 'Secure') {
25
+ useEffect(() => {
26
+ const handler = (data) => {
27
+ if (data?.repositoryName !== repositoryName) {
28
+ return;
29
+ }
30
+ onChange(data);
31
+ };
32
+ oneHatData.on(CROSS_TAB_EVENT_NAME, handler);
33
+ return () => {
34
+ oneHatData.off(CROSS_TAB_EVENT_NAME, handler);
35
+ };
36
+ }, [onChange, repositoryName]);
37
+ }