@xh/hoist 60.0.1 → 60.1.1
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 +16 -0
- package/admin/tabs/general/roles/RoleModel.ts +3 -3
- package/admin/tabs/general/roles/Types.ts +3 -8
- package/admin/tabs/general/roles/details/members/RoleMembers.ts +2 -2
- package/admin/tabs/general/roles/details/members/RoleMembersModel.ts +4 -4
- package/admin/tabs/general/roles/editor/form/RoleForm.ts +18 -13
- package/admin/tabs/general/roles/editor/form/RoleFormModel.ts +15 -2
- package/admin/tabs/general/roles/graph/RoleGraphModel.ts +2 -1
- package/cmp/ag-grid/AgGrid.scss +3 -0
- package/cmp/grid/columns/Column.ts +10 -37
- package/cmp/grid/columns/ColumnGroup.ts +2 -1
- package/cmp/grid/impl/Utils.ts +39 -5
- package/core/exception/Exception.ts +3 -2
- package/desktop/cmp/form/FormField.ts +5 -2
- package/package.json +1 -1
- package/svc/FetchService.ts +16 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 60.1.1 - 2024-01-29
|
|
4
|
+
|
|
5
|
+
### ⚙️ Technical
|
|
6
|
+
* Improve unique constraint validation of Roles and Role Members in Admin UI.
|
|
7
|
+
* Minor UI tweaks to admin UI
|
|
8
|
+
|
|
9
|
+
## 60.1.0 - 2024-01-18
|
|
10
|
+
|
|
11
|
+
### 🐞 Bug Fixes
|
|
12
|
+
* Fixed transparent background for popup inline editors.
|
|
13
|
+
* Exceptions that occur in custom application tooltips will now be caught and logged to console,
|
|
14
|
+
rather than crashing grid.
|
|
15
|
+
|
|
16
|
+
### ⚙️ Technical
|
|
17
|
+
* Improvements to exception handling during app initialization
|
|
18
|
+
|
|
3
19
|
## 60.0.1 - 2024-01-16
|
|
4
20
|
|
|
5
21
|
### 🐞 Bug Fixes
|
|
@@ -291,12 +291,12 @@ export class RoleModel extends HoistModel {
|
|
|
291
291
|
fieldSpecs: compact([
|
|
292
292
|
'name',
|
|
293
293
|
'category',
|
|
294
|
-
config.
|
|
295
|
-
config.
|
|
294
|
+
config.userAssignmentSupported && 'users',
|
|
295
|
+
config.directoryGroupsSupported && 'directoryGroups',
|
|
296
296
|
'roles',
|
|
297
297
|
'inheritedRoleNames',
|
|
298
298
|
'effectiveUserNames',
|
|
299
|
-
config.
|
|
299
|
+
config.directoryGroupsSupported && 'effectiveDirectoryGroupNames',
|
|
300
300
|
'effectiveRoleNames',
|
|
301
301
|
'lastUpdatedBy',
|
|
302
302
|
{
|
|
@@ -38,12 +38,7 @@ export type RoleMemberType = 'USER' | 'DIRECTORY_GROUP' | 'ROLE';
|
|
|
38
38
|
|
|
39
39
|
export interface RoleModuleConfig {
|
|
40
40
|
enabled: boolean;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
infoTooltips: {
|
|
45
|
-
users: string;
|
|
46
|
-
directoryGroups: string;
|
|
47
|
-
roles: string;
|
|
48
|
-
};
|
|
41
|
+
userAssignmentSupported: boolean;
|
|
42
|
+
directoryGroupsSupported: boolean;
|
|
43
|
+
directoryGroupsDescription: string;
|
|
49
44
|
}
|
|
@@ -104,11 +104,11 @@ const count = hoistCmp.factory<CountProps>(({count, icon}) =>
|
|
|
104
104
|
|
|
105
105
|
const bbar = hoistCmp.factory<RoleMembersModel>(({model}) => {
|
|
106
106
|
const {directCounts, moduleConfig} = model;
|
|
107
|
-
if (!moduleConfig?.
|
|
107
|
+
if (!moduleConfig?.directoryGroupsSupported && directCounts.DIRECTORY_GROUP) {
|
|
108
108
|
return warningBanner({
|
|
109
109
|
message: 'Directory Groups disabled. Will ignore.'
|
|
110
110
|
});
|
|
111
|
-
} else if (!moduleConfig?.
|
|
111
|
+
} else if (!moduleConfig?.userAssignmentSupported && directCounts.USER) {
|
|
112
112
|
return warningBanner({
|
|
113
113
|
message: 'Users assignment disabled. Will ignore.'
|
|
114
114
|
});
|
|
@@ -57,7 +57,7 @@ export class RoleMembersModel extends HoistModel {
|
|
|
57
57
|
const {effectiveUsers, effectiveDirectoryGroups, effectiveRoles} = this.selectedRole;
|
|
58
58
|
return {
|
|
59
59
|
USER: effectiveUsers.length,
|
|
60
|
-
DIRECTORY_GROUP: this.moduleConfig?.
|
|
60
|
+
DIRECTORY_GROUP: this.moduleConfig?.directoryGroupsSupported
|
|
61
61
|
? effectiveDirectoryGroups.length
|
|
62
62
|
: 0,
|
|
63
63
|
ROLE: effectiveRoles.length
|
|
@@ -105,7 +105,7 @@ export class RoleMembersModel extends HoistModel {
|
|
|
105
105
|
})),
|
|
106
106
|
|
|
107
107
|
// 2 - Directory Groups
|
|
108
|
-
...(this.moduleConfig?.
|
|
108
|
+
...(this.moduleConfig?.directoryGroupsSupported
|
|
109
109
|
? role.effectiveDirectoryGroups.map(it => ({
|
|
110
110
|
name: it.name,
|
|
111
111
|
sources: this.sortThisRoleFirst(it.sourceRoles.map(role => ({role}))),
|
|
@@ -234,8 +234,8 @@ export class RoleMembersModel extends HoistModel {
|
|
|
234
234
|
|
|
235
235
|
if (
|
|
236
236
|
!includeEffective &&
|
|
237
|
-
((type === types.USER && !moduleConfig.
|
|
238
|
-
(isDirectoryGroup && !moduleConfig.
|
|
237
|
+
((type === types.USER && !moduleConfig.userAssignmentSupported) ||
|
|
238
|
+
(isDirectoryGroup && !moduleConfig.directoryGroupsSupported))
|
|
239
239
|
) {
|
|
240
240
|
return {hidden: true};
|
|
241
241
|
}
|
|
@@ -68,12 +68,12 @@ const assignments = hoistCmp.factory<RoleFormModel>(({model}) =>
|
|
|
68
68
|
items: [
|
|
69
69
|
assignmentsPanel({
|
|
70
70
|
entity: 'USER',
|
|
71
|
-
omit: !model.moduleConfig?.
|
|
71
|
+
omit: !model.moduleConfig?.userAssignmentSupported && model.usersGridModel.empty
|
|
72
72
|
}),
|
|
73
73
|
assignmentsPanel({
|
|
74
74
|
entity: 'DIRECTORY_GROUP',
|
|
75
75
|
omit:
|
|
76
|
-
!model.moduleConfig?.
|
|
76
|
+
!model.moduleConfig?.directoryGroupsSupported &&
|
|
77
77
|
model.directoryGroupsGridModel.empty
|
|
78
78
|
}),
|
|
79
79
|
assignmentsPanel({entity: 'ROLE'})
|
|
@@ -138,13 +138,13 @@ const bbar = hoistCmp.factory<AssignmentsPanelProps>(({entity, model}) => {
|
|
|
138
138
|
return warningBanner({
|
|
139
139
|
compact: true,
|
|
140
140
|
message: 'Users assignment disabled. Will ignore.',
|
|
141
|
-
omit: model.moduleConfig?.
|
|
141
|
+
omit: model.moduleConfig?.userAssignmentSupported
|
|
142
142
|
});
|
|
143
143
|
case 'DIRECTORY_GROUP':
|
|
144
144
|
return warningBanner({
|
|
145
145
|
compact: true,
|
|
146
146
|
message: 'Directory Groups disabled. Will ignore.',
|
|
147
|
-
omit: model.moduleConfig?.
|
|
147
|
+
omit: model.moduleConfig?.directoryGroupsSupported
|
|
148
148
|
});
|
|
149
149
|
default:
|
|
150
150
|
return null;
|
|
@@ -153,15 +153,20 @@ const bbar = hoistCmp.factory<AssignmentsPanelProps>(({entity, model}) => {
|
|
|
153
153
|
|
|
154
154
|
const infoIcon = hoistCmp.factory<AssignmentsPanelProps>({
|
|
155
155
|
render({entity, model}) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
156
|
+
let tooltipText = null;
|
|
157
|
+
switch (entity) {
|
|
158
|
+
case 'USER':
|
|
159
|
+
tooltipText = 'All users listed here will be directly granted this role.';
|
|
160
|
+
break;
|
|
161
|
+
case 'DIRECTORY_GROUP':
|
|
162
|
+
tooltipText =
|
|
163
|
+
model.moduleConfig?.directoryGroupsDescription ??
|
|
164
|
+
'All members of these directory groups will be granted this role.';
|
|
165
|
+
break;
|
|
166
|
+
case 'ROLE':
|
|
167
|
+
tooltipText = 'All users holding these roles will also be granted this role.';
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
165
170
|
return tooltip({
|
|
166
171
|
item: Icon.info(),
|
|
167
172
|
content: tooltipText,
|
|
@@ -137,7 +137,7 @@ export class RoleFormModel extends HoistModel {
|
|
|
137
137
|
rules: [
|
|
138
138
|
required,
|
|
139
139
|
({value}) =>
|
|
140
|
-
this.invalidNames.
|
|
140
|
+
this.invalidNames.some(it => it.toLowerCase() === value?.toLowerCase())
|
|
141
141
|
? `Role "${value}" already exists.`
|
|
142
142
|
: null
|
|
143
143
|
]
|
|
@@ -159,7 +159,20 @@ export class RoleFormModel extends HoistModel {
|
|
|
159
159
|
},
|
|
160
160
|
columns: [
|
|
161
161
|
{
|
|
162
|
-
field: {
|
|
162
|
+
field: {
|
|
163
|
+
name: 'name',
|
|
164
|
+
rules: [
|
|
165
|
+
required,
|
|
166
|
+
({value, record}) =>
|
|
167
|
+
record.store.allRecords.some(
|
|
168
|
+
it =>
|
|
169
|
+
it !== record &&
|
|
170
|
+
it.get('name')?.toLowerCase() === value?.toLowerCase()
|
|
171
|
+
)
|
|
172
|
+
? `${value} already added.`
|
|
173
|
+
: null
|
|
174
|
+
]
|
|
175
|
+
},
|
|
163
176
|
flex: 1,
|
|
164
177
|
editable: true,
|
|
165
178
|
editor: props => {
|
|
@@ -15,7 +15,8 @@ export class RoleGraphModel extends HoistModel {
|
|
|
15
15
|
get relatedRoles(): EffectiveRoleMember[] {
|
|
16
16
|
const {role, relationship} = this;
|
|
17
17
|
if (!role) return [];
|
|
18
|
-
|
|
18
|
+
const ret = relationship === 'effective' ? role.effectiveRoles : role.inheritedRoles;
|
|
19
|
+
return ret.filter(it => it.name !== role.name);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
get role(): HoistRole {
|
package/cmp/ag-grid/AgGrid.scss
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
} from 'react';
|
|
43
43
|
import {GridModel} from '../GridModel';
|
|
44
44
|
import {GridSorter} from '../GridSorter';
|
|
45
|
-
import {managedRenderer} from '../impl/Utils';
|
|
45
|
+
import {getAgHeaderClassFn, managedRenderer} from '../impl/Utils';
|
|
46
46
|
import {
|
|
47
47
|
ColumnCellClassFn,
|
|
48
48
|
ColumnCellClassRuleFn,
|
|
@@ -62,10 +62,8 @@ import {
|
|
|
62
62
|
} from '../Types';
|
|
63
63
|
import {ExcelFormat} from '../enums/ExcelFormat';
|
|
64
64
|
import {FunctionComponent} from 'react';
|
|
65
|
-
import {ColumnGroup} from './ColumnGroup';
|
|
66
65
|
import type {
|
|
67
66
|
ColDef,
|
|
68
|
-
HeaderClassParams,
|
|
69
67
|
ITooltipParams,
|
|
70
68
|
ValueGetterParams,
|
|
71
69
|
ValueSetterParams
|
|
@@ -817,9 +815,15 @@ export class Column {
|
|
|
817
815
|
store
|
|
818
816
|
});
|
|
819
817
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
818
|
+
if (isFunction(tooltip)) {
|
|
819
|
+
try {
|
|
820
|
+
ret = tooltip(val, {record, gridModel, agParams, column: this});
|
|
821
|
+
} catch (e) {
|
|
822
|
+
logWarn([`Failure in tooltip for '${this.displayName}'`, e], 'Column');
|
|
823
|
+
}
|
|
824
|
+
} else {
|
|
825
|
+
ret = val;
|
|
826
|
+
}
|
|
823
827
|
}
|
|
824
828
|
|
|
825
829
|
const isElement = isValidElement(ret);
|
|
@@ -1083,34 +1087,3 @@ export class Column {
|
|
|
1083
1087
|
: record?.data[sortValue] ?? v;
|
|
1084
1088
|
}
|
|
1085
1089
|
}
|
|
1086
|
-
|
|
1087
|
-
export function getAgHeaderClassFn(
|
|
1088
|
-
column: Column | ColumnGroup
|
|
1089
|
-
): (params: HeaderClassParams) => string[] {
|
|
1090
|
-
// Generate CSS classes for headers.
|
|
1091
|
-
// Default alignment classes are mixed in with any provided custom classes.
|
|
1092
|
-
const {headerClass, headerAlign, gridModel} = column;
|
|
1093
|
-
|
|
1094
|
-
return agParams => {
|
|
1095
|
-
let r = [];
|
|
1096
|
-
if (headerClass) {
|
|
1097
|
-
r = castArray(
|
|
1098
|
-
isFunction(headerClass) ? headerClass({column, gridModel, agParams}) : headerClass
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
if (headerAlign === 'center' || headerAlign === 'right') {
|
|
1103
|
-
r.push('xh-column-header-align-' + headerAlign);
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
if (column instanceof Column && column.isTreeColumn && column.headerHasExpandCollapse) {
|
|
1107
|
-
r.push('xh-column-header--with-expand-collapse');
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
if (gridModel.headerMenuDisplay === 'hover') {
|
|
1111
|
-
r.push('xh-column-header--hoverable');
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
return r;
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {getAgHeaderClassFn} from '@xh/hoist/cmp/grid/impl/Utils';
|
|
7
8
|
import {HAlign, PlainObject, Some, Thunkable, XH} from '@xh/hoist/core';
|
|
8
9
|
import {genDisplayName} from '@xh/hoist/data';
|
|
9
10
|
|
|
@@ -13,7 +14,7 @@ import {clone, isEmpty, isFunction, isString, keysIn} from 'lodash';
|
|
|
13
14
|
import {ReactNode} from 'react';
|
|
14
15
|
import {GridModel} from '../GridModel';
|
|
15
16
|
import {ColumnHeaderClassFn, ColumnHeaderNameFn} from '../Types';
|
|
16
|
-
import {Column, ColumnSpec
|
|
17
|
+
import {Column, ColumnSpec} from './Column';
|
|
17
18
|
|
|
18
19
|
export interface ColumnGroupSpec {
|
|
19
20
|
/** Column or ColumnGroup configs for children of this group.*/
|
package/cmp/grid/impl/Utils.ts
CHANGED
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {ColumnRenderer, GroupRowRenderer} from '@xh/hoist/cmp/grid';
|
|
8
|
-
import {
|
|
7
|
+
import {Column, ColumnGroup, ColumnRenderer, GroupRowRenderer} from '@xh/hoist/cmp/grid';
|
|
8
|
+
import {HeaderClassParams} from '@xh/hoist/kit/ag-grid';
|
|
9
|
+
import {castArray, isFunction} from 'lodash';
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
* @internal
|
|
12
|
-
*/
|
|
11
|
+
/** @internal */
|
|
13
12
|
export function managedRenderer<T extends ColumnRenderer | GroupRowRenderer>(
|
|
14
13
|
fn: T,
|
|
15
14
|
identifier: string
|
|
@@ -24,3 +23,38 @@ export function managedRenderer<T extends ColumnRenderer | GroupRowRenderer>(
|
|
|
24
23
|
}
|
|
25
24
|
} as unknown as T;
|
|
26
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate CSS classes for headers.
|
|
29
|
+
* Default alignment classes are mixed in with any provided custom classes.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export function getAgHeaderClassFn(
|
|
34
|
+
column: Column | ColumnGroup
|
|
35
|
+
): (params: HeaderClassParams) => string[] {
|
|
36
|
+
const {headerClass, headerAlign, gridModel} = column;
|
|
37
|
+
|
|
38
|
+
return agParams => {
|
|
39
|
+
let r = [];
|
|
40
|
+
if (headerClass) {
|
|
41
|
+
r = castArray(
|
|
42
|
+
isFunction(headerClass) ? headerClass({column, gridModel, agParams}) : headerClass
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (headerAlign === 'center' || headerAlign === 'right') {
|
|
47
|
+
r.push('xh-column-header-align-' + headerAlign);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (column instanceof Column && column.isTreeColumn && column.headerHasExpandCollapse) {
|
|
51
|
+
r.push('xh-column-header--with-expand-collapse');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (gridModel.headerMenuDisplay === 'hover') {
|
|
55
|
+
r.push('xh-column-header--hoverable');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return r;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -168,12 +168,13 @@ export class Exception {
|
|
|
168
168
|
origin = match
|
|
169
169
|
? match[0]
|
|
170
170
|
: protocolPattern.test(XH.baseUrl)
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
? XH.baseUrl
|
|
172
|
+
: window.location.origin;
|
|
173
173
|
|
|
174
174
|
return this.createFetchException({
|
|
175
175
|
name: 'Server Unavailable',
|
|
176
176
|
message: `Unable to contact the server at ${origin}`,
|
|
177
|
+
isServerUnavailable: true,
|
|
177
178
|
fetchOptions,
|
|
178
179
|
cause
|
|
179
180
|
});
|
|
@@ -163,7 +163,7 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
|
|
|
163
163
|
if (disabled) classes.push('xh-form-field-disabled');
|
|
164
164
|
if (displayNotValid) classes.push('xh-form-field-invalid');
|
|
165
165
|
|
|
166
|
-
const testId = getFormFieldTestId(props, formContext, model
|
|
166
|
+
const testId = getFormFieldTestId(props, formContext, model?.name);
|
|
167
167
|
useOnMount(() => instanceManager.registerModelWithTestId(testId, model));
|
|
168
168
|
useOnUnmount(() => instanceManager.unregisterModelWithTestId(testId));
|
|
169
169
|
|
|
@@ -369,5 +369,8 @@ function getFormFieldTestId(
|
|
|
369
369
|
formContext: FormContextType,
|
|
370
370
|
fieldName: string
|
|
371
371
|
): string {
|
|
372
|
-
return
|
|
372
|
+
return (
|
|
373
|
+
props.testId ??
|
|
374
|
+
(formContext.testId && fieldName ? `${formContext.testId}-${fieldName}` : undefined)
|
|
375
|
+
);
|
|
373
376
|
}
|
package/package.json
CHANGED
package/svc/FetchService.ts
CHANGED
|
@@ -46,19 +46,22 @@ export class FetchService extends HoistService {
|
|
|
46
46
|
try {
|
|
47
47
|
await this.fetch({url: 'ping'});
|
|
48
48
|
} catch (e) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
if (e.isServerUnavailable) {
|
|
50
|
+
const {baseUrl} = XH,
|
|
51
|
+
pingURL = baseUrl.startsWith('http')
|
|
52
|
+
? `${baseUrl}ping`
|
|
53
|
+
: `${window.location.origin}${baseUrl}ping`;
|
|
54
|
+
|
|
55
|
+
throw XH.exception({
|
|
56
|
+
name: 'UI Server Unavailable',
|
|
57
|
+
detail: e.message,
|
|
58
|
+
message:
|
|
59
|
+
'Client cannot reach UI server. Please check UI server at the ' +
|
|
60
|
+
`following location: ${pingURL}`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw e;
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
|