@onehat/ui 0.3.333 → 0.3.335

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.
@@ -2,11 +2,22 @@ import { useEffect, useState, useRef, } from 'react';
2
2
  import {
3
3
  Button,
4
4
  } from 'native-base';
5
+ import {
6
+ ADD,
7
+ EDIT,
8
+ DELETE,
9
+ VIEW,
10
+ COPY,
11
+ DUPLICATE,
12
+ PRINT,
13
+ UPLOAD_DOWNLOAD,
14
+ } from '../../Constants/Commands.js';
5
15
  import {
6
16
  EDITOR_MODE__VIEW,
7
17
  EDITOR_MODE__ADD,
8
18
  EDITOR_MODE__EDIT,
9
19
  EDITOR_TYPE__SIDE,
20
+ EDITOR_TYPE__INLINE,
10
21
  } from '../../Constants/Editor.js';
11
22
  import UiGlobals from '../../UiGlobals.js';
12
23
  import _ from 'lodash';
@@ -20,7 +31,7 @@ export default function withEditor(WrappedComponent, isTree = false) {
20
31
 
21
32
  let [editorMode, setEditorMode] = useState(EDITOR_MODE__VIEW); // Can change below, so use 'let'
22
33
  const {
23
- userCanEdit = true,
34
+ userCanEdit = true, // not permissions, but capability
24
35
  userCanView = true,
25
36
  canEditorViewOnly = false, // whether the editor can *ever* change state out of 'View' mode
26
37
  disableAdd = false,
@@ -56,6 +67,10 @@ export default function withEditor(WrappedComponent, isTree = false) {
56
67
  // withData
57
68
  Repository,
58
69
 
70
+ // withPermissions
71
+ canUser,
72
+ showPermissionsError,
73
+
59
74
  // withSelection
60
75
  selection,
61
76
  setSelection,
@@ -108,6 +123,11 @@ export default function withEditor(WrappedComponent, isTree = false) {
108
123
  return newEntityDisplayValueRef.current;
109
124
  },
110
125
  doAdd = async (e, values) => {
126
+ if (canUser && !canUser(ADD)) {
127
+ showPermissionsError(ADD);
128
+ return;
129
+ }
130
+
111
131
  let addValues = values;
112
132
 
113
133
  if (Repository?.isLoading) {
@@ -201,6 +221,10 @@ export default function withEditor(WrappedComponent, isTree = false) {
201
221
  setIsEditorShown(true);
202
222
  },
203
223
  doEdit = async () => {
224
+ if (canUser && !canUser(EDIT)) {
225
+ showPermissionsError(EDIT);
226
+ return;
227
+ }
204
228
  if (_.isEmpty(selection) || (_.isArray(selection) && (selection.length > 1 || selection[0]?.isDestroyed))) {
205
229
  return;
206
230
  }
@@ -215,6 +239,10 @@ export default function withEditor(WrappedComponent, isTree = false) {
215
239
  setIsEditorShown(true);
216
240
  },
217
241
  doDelete = async (args) => {
242
+ if (canUser && !canUser(DELETE)) {
243
+ showPermissionsError(DELETE);
244
+ return;
245
+ }
218
246
  let cb = null;
219
247
  if (_.isFunction(args)) {
220
248
  cb = args;
@@ -267,6 +295,10 @@ export default function withEditor(WrappedComponent, isTree = false) {
267
295
  deleteRecord(false, cb);
268
296
  },
269
297
  deleteRecord = async (moveSubtreeUp, cb) => {
298
+ if (canUser && !canUser(DELETE)) {
299
+ showPermissionsError(DELETE);
300
+ return;
301
+ }
270
302
  if (getListeners().onBeforeDelete) {
271
303
  const listenerResult = await getListeners().onBeforeDelete(selection);
272
304
  if (listenerResult === false) {
@@ -296,6 +328,17 @@ export default function withEditor(WrappedComponent, isTree = false) {
296
328
  if (!userCanView) {
297
329
  return;
298
330
  }
331
+ if (canUser && !canUser(VIEW)) {
332
+ showPermissionsError(VIEW);
333
+ return;
334
+ }
335
+ if (editorType === EDITOR_TYPE__INLINE) {
336
+ alert('Cannot view in inline editor.');
337
+ return; // inline editor doesn't have a view mode
338
+ }
339
+
340
+ // check permissions for view
341
+
299
342
  if (selection.length !== 1) {
300
343
  return;
301
344
  }
@@ -311,6 +354,14 @@ export default function withEditor(WrappedComponent, isTree = false) {
311
354
  if (!userCanEdit || disableDuplicate) {
312
355
  return;
313
356
  }
357
+ if (canUser && !canUser(DUPLICATE)) {
358
+ showPermissionsError(DUPLICATE);
359
+ return;
360
+ }
361
+
362
+ // check permissions for duplicate
363
+
364
+
314
365
  if (selection.length !== 1) {
315
366
  return;
316
367
  }
@@ -339,6 +390,12 @@ export default function withEditor(WrappedComponent, isTree = false) {
339
390
  doEdit();
340
391
  },
341
392
  doEditorSave = async (data, e) => {
393
+ let mode = editorMode === EDITOR_MODE__ADD ? ADD : EDIT;
394
+ if (canUser && !canUser(mode)) {
395
+ showPermissionsError(mode);
396
+ return;
397
+ }
398
+
342
399
  // NOTE: The Form submits onSave for both adds (when not isAutoSsave) and edits.
343
400
  const isSingle = selection.length === 1;
344
401
  let useStaged = false;
@@ -395,7 +452,11 @@ export default function withEditor(WrappedComponent, isTree = false) {
395
452
  await getListeners().onAfterAddSave(selection);
396
453
  }
397
454
  setIsAdding(false);
398
- setEditorMode(EDITOR_MODE__EDIT);
455
+ if (!canUser || canUser(EDIT)) {
456
+ setEditorMode(EDITOR_MODE__EDIT);
457
+ } else {
458
+ setEditorMode(EDITOR_MODE__VIEW);
459
+ }
399
460
  } else if (editorMode === EDITOR_MODE__EDIT) {
400
461
  if (getListeners().onAfterEdit) {
401
462
  await getListeners().onAfterEdit(selection);
@@ -440,6 +501,11 @@ export default function withEditor(WrappedComponent, isTree = false) {
440
501
  setIsEditorShown(false);
441
502
  },
442
503
  doEditorDelete = async () => {
504
+ if (canUser && !canUser(DELETE)) {
505
+ showPermissionsError(DELETE);
506
+ return;
507
+ }
508
+
443
509
  doDelete(() => {
444
510
  setEditorMode(EDITOR_MODE__VIEW);
445
511
  setIsEditorShown(false);
@@ -484,9 +550,19 @@ export default function withEditor(WrappedComponent, isTree = false) {
484
550
  return mode;
485
551
  },
486
552
  setEditMode = () => {
553
+ if (canUser && !canUser(EDIT)) {
554
+ showPermissionsError(EDIT);
555
+ return;
556
+ }
557
+
487
558
  setEditorMode(EDITOR_MODE__EDIT);
488
559
  },
489
560
  setViewMode = () => {
561
+ if (canUser && !canUser(VIEW)) {
562
+ showPermissionsError(VIEW);
563
+ return;
564
+ }
565
+
490
566
  function doIt() {
491
567
  setEditorMode(EDITOR_MODE__VIEW);
492
568
  }
@@ -4,6 +4,9 @@ import {
4
4
  Row,
5
5
  Text,
6
6
  } from 'native-base';
7
+ import {
8
+ VIEW,
9
+ } from '../../Constants/Commands.js';
7
10
  import * as yup from 'yup'; // https://github.com/jquense/yup#string
8
11
  import Inflector from 'inflector-js';
9
12
  import qs from 'qs';
@@ -19,15 +22,21 @@ import _ from 'lodash';
19
22
  export default function withPdfButtons(WrappedComponent) {
20
23
  return withModal((props) => {
21
24
 
25
+ let showButtons = true;
22
26
  if (!props.showPdfBtns) {
27
+ showButtons = false;
28
+ }
29
+ if (props.canUser && !props.canUser(VIEW)) { // permissions
30
+ showButtons = false;
31
+ }
32
+ if (!showButtons) {
23
33
  // bypass everything.
24
34
  // If we don't do this, we get an infinite recursion with Form
25
35
  // because this HOC wraps Form and uses Form itself.
26
36
  return <WrappedComponent {...props} />;
27
37
  }
28
38
 
29
- const
30
- {
39
+ const {
31
40
  additionalEditButtons = [],
32
41
  additionalViewButtons = [],
33
42
  items = [],
@@ -0,0 +1,110 @@
1
+ import Inflector from 'inflector-js';
2
+ import inArray from '../../Functions/inArray.js';
3
+ import {
4
+ ADD,
5
+ EDIT,
6
+ DELETE,
7
+ VIEW,
8
+ COPY,
9
+ DUPLICATE,
10
+ PRINT,
11
+ UPLOAD_DOWNLOAD,
12
+ } from '../../Constants/Commands.js';
13
+ import UiGlobals from '../../UiGlobals.js';
14
+ import _ from 'lodash';
15
+
16
+
17
+ export default function withPermissions(WrappedComponent, forceUsePermissions = false) {
18
+ return (props) => {
19
+
20
+ if (!props.usePermissions && !forceUsePermissions) {
21
+ return <WrappedComponent {...props} />;
22
+ }
23
+
24
+ const {
25
+ // withAlert
26
+ alert,
27
+
28
+ // withData
29
+ Repository,
30
+ } = props,
31
+ model = Repository?.schema?.name,
32
+ checkPermission = (permission) => {
33
+ const
34
+ reduxState = UiGlobals.redux?.getState(),
35
+ permissions = reduxState?.app?.permissions;
36
+ if (!permissions) {
37
+ return false;
38
+ }
39
+ return inArray(permission, permissions);
40
+ },
41
+
42
+ showPermissionsError = (permission, modelForAlert = null) => {
43
+ if (!modelForAlert) {
44
+ modelForAlert = model; // use default model if none supplied
45
+ }
46
+ modelForAlert = Inflector.humanize(Inflector.underscore(modelForAlert)); // 'PmEvents' -> 'pm events'
47
+
48
+ alert(`You are not authorized to ${permission} ${modelForAlert}.`);
49
+ },
50
+
51
+ /**
52
+ * Check if user has permission to perform an action
53
+ *
54
+ * Example usages:
55
+ * canUser('view') // check if user can perform 'view' action on the default model
56
+ * canUser('add', 'PmEvents') // check if user can perform 'add' action on a specific model
57
+ * canUser('do_something_else) // check if user has a custom permission
58
+ *
59
+ * @param {string} permission - The permission to check for.
60
+ * @param {string} modelToCheck - The model to check for the permission on.
61
+ * @returns {boolean} - Whether user has permission
62
+ */
63
+ canUser = (permission, modelToCheck = null) => {
64
+
65
+ // deal with special cases that refer to other permissions
66
+ switch(permission) {
67
+ case PRINT:
68
+ permission = VIEW;
69
+ break;
70
+ case COPY:
71
+ case DUPLICATE: {
72
+ // user must have ADD _and_ EDIT permissions, so check both
73
+ const
74
+ hasAddPermission = canUser(ADD, modelToCheck),
75
+ hasEditPermission = canUser(EDIT, modelToCheck);
76
+ return hasAddPermission && hasEditPermission;
77
+ }
78
+ case UPLOAD_DOWNLOAD: {
79
+ // user must have VIEW, ADD, EDIT, and DELETE permissions, so check all of them
80
+ const
81
+ hasViewPermission = canUser(VIEW, modelToCheck),
82
+ hasAddPermission = canUser(ADD, modelToCheck),
83
+ hasEditPermission = canUser(EDIT, modelToCheck),
84
+ hasDeletePermission = canUser(DELETE, modelToCheck);
85
+ return hasViewPermission && hasAddPermission && hasEditPermission && hasDeletePermission;
86
+ }
87
+ default:
88
+ // do nothing
89
+ break;
90
+ }
91
+
92
+ // standard CRUD permissions
93
+ if (inArray(permission, [VIEW, ADD, EDIT, DELETE])) {
94
+ if (!modelToCheck) {
95
+ modelToCheck = model; // use default model if none supplied
96
+ }
97
+ modelToCheck = Inflector.underscore(modelToCheck); // 'PmEvents' -> 'pm_events'
98
+ permission += '_' + modelToCheck; // e.g. 'view_pm_events'
99
+ }
100
+
101
+ return checkPermission(permission);
102
+ };
103
+
104
+ return <WrappedComponent
105
+ {...props}
106
+ canUser={canUser}
107
+ showPermissionsError={showPermissionsError}
108
+ />;
109
+ };
110
+ }
@@ -2,6 +2,16 @@ import React, { useState, useEffect, } from 'react';
2
2
  import {
3
3
  Modal,
4
4
  } from 'native-base';
5
+ import {
6
+ ADD,
7
+ EDIT,
8
+ DELETE,
9
+ VIEW,
10
+ COPY,
11
+ DUPLICATE,
12
+ PRINT,
13
+ UPLOAD_DOWNLOAD,
14
+ } from '../../constants/Commands.js';
5
15
  import Clipboard from '../Icons/Clipboard.js';
6
16
  import Duplicate from '../Icons/Duplicate.js';
7
17
  import Edit from '../Icons/Edit.js';
@@ -18,14 +28,14 @@ import _ from 'lodash';
18
28
  // and a toolbar button that match in text label, icon, and handler.
19
29
 
20
30
  const presetButtons = [
21
- 'view',
22
- 'add',
23
- 'edit',
24
- 'delete',
25
- 'duplicate',
26
- 'copy',
27
- // 'print',
28
- 'uploadDownload',
31
+ ADD,
32
+ EDIT,
33
+ DELETE,
34
+ VIEW,
35
+ COPY,
36
+ DUPLICATE,
37
+ // PRINT,
38
+ UPLOAD_DOWNLOAD,
29
39
  ];
30
40
 
31
41
  export default function withPresetButtons(WrappedComponent, isGrid = false) {
@@ -42,9 +52,9 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
42
52
  additionalToolbarButtons = [],
43
53
  useUploadDownload = false,
44
54
  onChangeColumnsConfig,
45
- verifyCanEdit,
46
- verifyCanDelete,
47
- verifyCanDuplicate,
55
+ canRecordBeEdited,
56
+ canRecordBeDeleted,
57
+ canRecordBeDuplicated,
48
58
  ...propsToPass
49
59
  } = props,
50
60
  {
@@ -71,8 +81,11 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
71
81
  // withData
72
82
  Repository,
73
83
 
84
+ // withPermissions
85
+ canUser,
86
+
74
87
  // withEditor
75
- userCanEdit = true,
88
+ userCanEdit = true, // not permissions, but capability
76
89
  userCanView = true,
77
90
  onAdd,
78
91
  onEdit,
@@ -101,44 +114,60 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
101
114
  isTypeDisabledCompletely = (type) => {
102
115
  let isDisabled = false;
103
116
  switch(type) {
104
- case 'add':
117
+ case ADD:
105
118
  if (disableAdd || canEditorViewOnly) {
106
119
  isDisabled = true;
120
+ } else if (canUser && !canUser(ADD)) { // check Permissions
121
+ isDisabled = true;
107
122
  }
108
123
  break;
109
- case 'edit':
124
+ case EDIT:
110
125
  if (disableEdit || canEditorViewOnly || isSideEditor) {
111
126
  isDisabled = true;
127
+ } else if (canUser && !canUser(EDIT)) { // check Permissions
128
+ isDisabled = true;
112
129
  }
113
130
  break;
114
- case 'delete':
131
+ case DELETE:
115
132
  if (disableDelete || canEditorViewOnly) {
116
133
  isDisabled = true;
134
+ } else if (canUser && !canUser(DELETE)) { // check Permissions
135
+ isDisabled = true;
117
136
  }
118
137
  break;
119
- case 'view':
138
+ case VIEW:
120
139
  if (disableView || isSideEditor) {
121
140
  isDisabled = true;
141
+ } else if (canUser && !canUser(VIEW)) { // check Permissions
142
+ isDisabled = true;
122
143
  }
123
144
  break;
124
- case 'copy':
145
+ case COPY:
125
146
  if (disableCopy) {
126
147
  isDisabled = true;
148
+ } else if (canUser && !canUser(COPY)) { // check Permissions
149
+ isDisabled = true;
127
150
  }
128
151
  break;
129
- case 'duplicate':
152
+ case DUPLICATE:
130
153
  if (disableDuplicate || canEditorViewOnly) {
131
154
  isDisabled = true;
155
+ } else if (canUser && !canUser(DUPLICATE)) { // check Permissions
156
+ isDisabled = true;
132
157
  }
133
158
  break;
134
- case 'print':
159
+ case PRINT:
135
160
  if (disablePrint) {
136
161
  isDisabled = true;
162
+ } else if (canUser && !canUser(PRINT)) { // check Permissions
163
+ isDisabled = true;
137
164
  }
138
165
  break;
139
- case 'uploadDownload':
166
+ case UPLOAD_DOWNLOAD:
140
167
  if (!useUploadDownload) {
141
168
  isDisabled = true;
169
+ } else if (canUser && !canUser(UPLOAD_DOWNLOAD)) { // check Permissions
170
+ isDisabled = true;
142
171
  }
143
172
  break;
144
173
  default:
@@ -152,7 +181,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
152
181
  icon = null,
153
182
  isDisabled = false;
154
183
  switch(type) {
155
- case 'add':
184
+ case ADD:
156
185
  key = 'addBtn';
157
186
  text = 'Add';
158
187
  handler = onAdd;
@@ -164,7 +193,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
164
193
  isDisabled = true;
165
194
  }
166
195
  break;
167
- case 'edit':
196
+ case EDIT:
168
197
  key = 'editBtn';
169
198
  text = 'Edit';
170
199
  handler = onEdit;
@@ -175,11 +204,11 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
175
204
  if (_.isEmpty(selection) || (_.isArray(selection) && selection.length > 1)) {
176
205
  isDisabled = true;
177
206
  }
178
- if (verifyCanEdit && !verifyCanEdit(selection)) {
207
+ if (canRecordBeEdited && !canRecordBeEdited(selection)) {
179
208
  isDisabled = true;
180
209
  }
181
210
  break;
182
- case 'delete':
211
+ case DELETE:
183
212
  key = 'deleteBtn';
184
213
  text = 'Delete';
185
214
  handler = onDelete;
@@ -190,7 +219,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
190
219
  if (_.isEmpty(selection) || (_.isArray(selection) && selection.length > 1)) {
191
220
  isDisabled = true;
192
221
  }
193
- if (verifyCanDelete && !verifyCanDelete(selection)) {
222
+ if (canRecordBeDeleted && !canRecordBeDeleted(selection)) {
194
223
  isDisabled = true;
195
224
  }
196
225
  if (isTree) {
@@ -200,7 +229,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
200
229
  }
201
230
  }
202
231
  break;
203
- case 'view':
232
+ case VIEW:
204
233
  key = 'viewBtn';
205
234
  text = 'View';
206
235
  handler = onView;
@@ -213,7 +242,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
213
242
  isDisabled = true;
214
243
  }
215
244
  break;
216
- case 'copy':
245
+ case COPY:
217
246
  key = 'copyBtn';
218
247
  text = 'Copy to Clipboard';
219
248
  handler = onCopyToClipboard;
@@ -226,7 +255,7 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
226
255
  isDisabled = true;
227
256
  }
228
257
  break;
229
- case 'duplicate':
258
+ case DUPLICATE:
230
259
  key = 'duplicateBtn';
231
260
  text = 'Duplicate';
232
261
  handler = onDuplicate;
@@ -238,16 +267,16 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
238
267
  if (_.isEmpty(selection) || selection.length > 1) {
239
268
  isDisabled = true;
240
269
  }
241
- if (verifyCanDuplicate && !verifyCanDuplicate(selection)) {
270
+ if (canRecordBeDuplicated && !canRecordBeDuplicated(selection)) {
242
271
  isDisabled = true;
243
272
  }
244
273
  break;
245
- // case 'print':
274
+ // case PRINT:
246
275
  // text = 'Print';
247
276
  // handler = onPrint;
248
277
  // icon = <Print />;
249
278
  // break;
250
- case 'uploadDownload':
279
+ case UPLOAD_DOWNLOAD:
251
280
  key = 'uploadDownloadBtn';
252
281
  text = 'Upload/Download';
253
282
  handler = onUploadDownload;
@@ -274,8 +303,8 @@ export default function withPresetButtons(WrappedComponent, isGrid = false) {
274
303
  if (isTypeDisabledCompletely(type)) { // i.e. not just temporarily disabled because of selection
275
304
  return;
276
305
  }
277
- if ((!userCanEdit && inArray(type, ['add', 'edit', 'delete', 'duplicate',])) ||
278
- (!userCanView && type === 'view')) {
306
+ if ((!userCanEdit && inArray(type, [ADD, EDIT, DELETE, DUPLICATE,])) ||
307
+ (!userCanView && type === VIEW)) {
279
308
  return;
280
309
  }
281
310
 
@@ -0,0 +1,17 @@
1
+ import {
2
+ Column,
3
+ Text,
4
+ } from 'native-base';
5
+
6
+ export default function Unauthorized(props) {
7
+ const
8
+ {
9
+ text = 'Unauthorized.',
10
+ } = props;
11
+ return <Column justifyContent="center" alignItems="center" w="100%" flex={1}>
12
+ <Text
13
+ textAlign="center"
14
+ color="#f00"
15
+ >{text}</Text>
16
+ </Column>;
17
+ }