@nu-art/permissions-frontend 0.401.9 → 0.500.6
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/_entity/access-group/ModuleFE_AccessGroup.d.ts +12 -0
- package/_entity/access-group/ModuleFE_AccessGroup.js +15 -0
- package/_entity/access-group/module-pack.d.ts +1 -0
- package/_entity/access-group/module-pack.js +2 -0
- package/_entity/permission-scope/ModuleFE_PermissionScope.d.ts +12 -0
- package/_entity/permission-scope/ModuleFE_PermissionScope.js +15 -0
- package/_entity/permission-scope/module-pack.d.ts +1 -0
- package/_entity/permission-scope/module-pack.js +2 -0
- package/_entity/user-permissions/ModuleFE_UserPermissions.d.ts +16 -0
- package/_entity/user-permissions/ModuleFE_UserPermissions.js +83 -0
- package/_entity/user-permissions/module-pack.d.ts +1 -0
- package/_entity/user-permissions/module-pack.js +2 -0
- package/consts.d.ts +1 -2
- package/consts.js +0 -1
- package/core/module-pack.js +6 -7
- package/index.d.ts +13 -3
- package/index.js +13 -3
- package/modules/ModuleFE_PermissionsAssert.d.ts +4 -27
- package/modules/ModuleFE_PermissionsAssert.js +12 -58
- package/package.json +22 -12
- package/ui/Page_Permissions/Page_Permissions.d.ts +2 -0
- package/ui/Page_Permissions/Page_Permissions.js +186 -0
- package/ui/Page_Permissions/Page_Permissions.scss +217 -0
- package/ui/Page_Permissions/route.d.ts +2 -0
- package/ui/Page_Permissions/route.js +6 -0
- package/ui/PermissionGuard.d.ts +19 -0
- package/ui/PermissionGuard.js +16 -0
- package/ui/scope-editor/Component_ScopeLabels.d.ts +6 -0
- package/ui/scope-editor/Component_ScopeLabels.js +12 -0
- package/ui/scope-editor/Component_ScopeMultiSelect.d.ts +26 -0
- package/ui/scope-editor/Component_ScopeMultiSelect.js +109 -0
- package/ui/scope-editor/Component_ScopeMultiSelect.scss +151 -0
- package/ui/scope-editor/scope-utils.d.ts +7 -0
- package/ui/scope-editor/scope-utils.js +38 -0
- package/PermissionKey_FE.d.ts +0 -24
- package/PermissionKey_FE.js +0 -23
- package/_entity/permission-access-level/ModuleFE_PermissionAccessLevel.d.ts +0 -11
- package/_entity/permission-access-level/ModuleFE_PermissionAccessLevel.js +0 -12
- package/_entity/permission-access-level/index.d.ts +0 -2
- package/_entity/permission-access-level/index.js +0 -2
- package/_entity/permission-access-level/module-pack.d.ts +0 -1
- package/_entity/permission-access-level/module-pack.js +0 -2
- package/_entity/permission-access-level/shared.d.ts +0 -1
- package/_entity/permission-access-level/shared.js +0 -1
- package/_entity/permission-access-level/ui-components.d.ts +0 -37
- package/_entity/permission-access-level/ui-components.js +0 -21
- package/_entity/permission-api/ModuleFE_PermissionAPI.d.ts +0 -11
- package/_entity/permission-api/ModuleFE_PermissionAPI.js +0 -12
- package/_entity/permission-api/index.d.ts +0 -2
- package/_entity/permission-api/index.js +0 -2
- package/_entity/permission-api/module-pack.d.ts +0 -1
- package/_entity/permission-api/module-pack.js +0 -2
- package/_entity/permission-api/shared.d.ts +0 -1
- package/_entity/permission-api/shared.js +0 -1
- package/_entity/permission-api/ui-components.d.ts +0 -37
- package/_entity/permission-api/ui-components.js +0 -21
- package/_entity/permission-domain/ModuleFE_PermissionDomain.d.ts +0 -9
- package/_entity/permission-domain/ModuleFE_PermissionDomain.js +0 -10
- package/_entity/permission-domain/index.d.ts +0 -2
- package/_entity/permission-domain/index.js +0 -2
- package/_entity/permission-domain/module-pack.d.ts +0 -1
- package/_entity/permission-domain/module-pack.js +0 -2
- package/_entity/permission-domain/shared.d.ts +0 -1
- package/_entity/permission-domain/shared.js +0 -1
- package/_entity/permission-domain/ui-components.d.ts +0 -37
- package/_entity/permission-domain/ui-components.js +0 -21
- package/_entity/permission-group/ModuleFE_PermissionGroup.d.ts +0 -11
- package/_entity/permission-group/ModuleFE_PermissionGroup.js +0 -12
- package/_entity/permission-group/index.d.ts +0 -2
- package/_entity/permission-group/index.js +0 -2
- package/_entity/permission-group/module-pack.d.ts +0 -1
- package/_entity/permission-group/module-pack.js +0 -2
- package/_entity/permission-group/shared.d.ts +0 -1
- package/_entity/permission-group/shared.js +0 -1
- package/_entity/permission-group/ui-components.d.ts +0 -37
- package/_entity/permission-group/ui-components.js +0 -21
- package/_entity/permission-project/ModuleFE_PermissionProject.d.ts +0 -11
- package/_entity/permission-project/ModuleFE_PermissionProject.js +0 -12
- package/_entity/permission-project/index.d.ts +0 -2
- package/_entity/permission-project/index.js +0 -2
- package/_entity/permission-project/module-pack.d.ts +0 -1
- package/_entity/permission-project/module-pack.js +0 -2
- package/_entity/permission-project/shared.d.ts +0 -1
- package/_entity/permission-project/shared.js +0 -1
- package/_entity/permission-project/ui-components.d.ts +0 -37
- package/_entity/permission-project/ui-components.js +0 -21
- package/_entity/permission-user/ModuleFE_PermissionUser.d.ts +0 -11
- package/_entity/permission-user/ModuleFE_PermissionUser.js +0 -14
- package/_entity/permission-user/index.d.ts +0 -2
- package/_entity/permission-user/index.js +0 -2
- package/_entity/permission-user/module-pack.d.ts +0 -1
- package/_entity/permission-user/module-pack.js +0 -2
- package/_entity/permission-user/shared.d.ts +0 -1
- package/_entity/permission-user/shared.js +0 -1
- package/_entity/permission-user/ui-components.d.ts +0 -37
- package/_entity/permission-user/ui-components.js +0 -36
- package/_entity.d.ts +0 -12
- package/_entity.js +0 -18
- package/core/permission-keys.d.ts +0 -5
- package/core/permission-keys.js +0 -6
- package/shared.d.ts +0 -1
- package/shared.js +0 -19
- package/ui/ATS_ComponentPermissionKeys/ATS_ComponentPermissionKeys.d.ts +0 -11
- package/ui/ATS_ComponentPermissionKeys/ATS_ComponentPermissionKeys.js +0 -22
- package/ui/ATS_ComponentPermissionKeys/permission-keys-editor.scss +0 -153
- package/ui/ATS_ComponentPermissionKeys/subEditors/Component_AccessLevelsEditor.d.ts +0 -16
- package/ui/ATS_ComponentPermissionKeys/subEditors/Component_AccessLevelsEditor.js +0 -38
- package/ui/ATS_ComponentPermissionKeys/subEditors/permission-keys-editor.d.ts +0 -16
- package/ui/ATS_ComponentPermissionKeys/subEditors/permission-keys-editor.js +0 -33
- package/ui/ATS_Permissions/ATS_Permissions.d.ts +0 -8
- package/ui/ATS_Permissions/ATS_Permissions.js +0 -63
- package/ui/ATS_Permissions/ATS_Permissions.scss +0 -172
- package/ui/Component_SwitchView.d.ts +0 -23
- package/ui/Component_SwitchView.js +0 -32
- package/ui/Component_SwitchViewV2.d.ts +0 -21
- package/ui/Component_SwitchViewV2.js +0 -25
- package/ui/PermissionsComponent.d.ts +0 -21
- package/ui/PermissionsComponent.js +0 -42
- package/ui/PermissionsEditableComponent.d.ts +0 -18
- package/ui/PermissionsEditableComponent.js +0 -21
- package/ui/Renderer_RoleNames.d.ts +0 -1
- package/ui/Renderer_RoleNames.js +0 -7
- package/ui/index.d.ts +0 -8
- package/ui/index.js +0 -8
- package/ui/permission-editors/components.d.ts +0 -6
- package/ui/permission-editors/components.js +0 -9
- package/ui/permission-editors/editor-base.d.ts +0 -18
- package/ui/permission-editors/editor-base.js +0 -17
- package/ui/permission-editors/editor-base.scss +0 -298
- package/ui/permission-editors/permission-api-edior/permission-api-editor.d.ts +0 -16
- package/ui/permission-editors/permission-api-edior/permission-api-editor.js +0 -63
- package/ui/permission-editors/permission-api-edior/permission-api-editor.scss +0 -7
- package/ui/permission-editors/permission-domains-editor.d.ts +0 -9
- package/ui/permission-editors/permission-domains-editor.js +0 -139
- package/ui/permission-editors/permission-groups-editor.d.ts +0 -9
- package/ui/permission-editors/permission-groups-editor.js +0 -80
- package/ui/permission-editors/permission-project-editor/permission-project-editor.scss +0 -10
- package/ui/permission-editors/permission-project-editor/permission-projects-editor.d.ts +0 -10
- package/ui/permission-editors/permission-project-editor/permission-projects-editor.js +0 -52
- package/ui/permission-editors/permission-users-editor.d.ts +0 -9
- package/ui/permission-editors/permission-users-editor.js +0 -70
- package/ui/ui-props.d.ts +0 -16
- package/ui/ui-props.js +0 -62
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
.page-permissions {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: row;
|
|
4
|
+
align-items: stretch;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ── Left panel: group list ──
|
|
11
|
+
|
|
12
|
+
.page-permissions__list-panel {
|
|
13
|
+
width: 280px;
|
|
14
|
+
min-width: 280px;
|
|
15
|
+
border-right: 1px solid var(--border-default);
|
|
16
|
+
padding: var(--space-5);
|
|
17
|
+
gap: var(--space-3);
|
|
18
|
+
overflow-y: auto;
|
|
19
|
+
align-items: stretch;
|
|
20
|
+
background: var(--bg-primary);
|
|
21
|
+
|
|
22
|
+
h2 {
|
|
23
|
+
font-size: var(--text-lg);
|
|
24
|
+
font-weight: var(--font-weight-semibold);
|
|
25
|
+
color: var(--text-primary);
|
|
26
|
+
margin: 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.page-permissions__group-list {
|
|
31
|
+
gap: 0;
|
|
32
|
+
align-items: stretch;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.page-permissions__group-row {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
gap: var(--space-1);
|
|
39
|
+
padding: var(--space-3);
|
|
40
|
+
border-radius: var(--radius-md);
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
transition: background var(--transition-fast);
|
|
43
|
+
border-left: 3px solid transparent;
|
|
44
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
45
|
+
|
|
46
|
+
&:last-child {
|
|
47
|
+
border-bottom: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
&:hover {
|
|
51
|
+
background: var(--bg-secondary);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.page-permissions__group-row--selected {
|
|
56
|
+
background: var(--accent-subtle);
|
|
57
|
+
border-left-color: var(--accent-primary);
|
|
58
|
+
|
|
59
|
+
&:hover {
|
|
60
|
+
background: var(--accent-subtle);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.page-permissions__group-row-header {
|
|
65
|
+
justify-content: space-between;
|
|
66
|
+
align-items: center;
|
|
67
|
+
gap: var(--space-2);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.page-permissions__group-row-name {
|
|
71
|
+
font-size: var(--text-sm);
|
|
72
|
+
font-weight: var(--font-weight-medium);
|
|
73
|
+
color: var(--text-primary);
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
text-overflow: ellipsis;
|
|
76
|
+
white-space: nowrap;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.page-permissions__group-row-meta {
|
|
80
|
+
font-size: var(--text-xs);
|
|
81
|
+
color: var(--text-tertiary);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Right panel: group detail ──
|
|
85
|
+
|
|
86
|
+
.page-permissions__detail-panel {
|
|
87
|
+
flex: 1;
|
|
88
|
+
padding: var(--space-6);
|
|
89
|
+
gap: var(--space-5);
|
|
90
|
+
overflow-y: auto;
|
|
91
|
+
align-items: stretch;
|
|
92
|
+
min-width: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.page-permissions__detail-panel--empty {
|
|
96
|
+
justify-content: center;
|
|
97
|
+
align-items: center;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.page-permissions__detail-header {
|
|
101
|
+
justify-content: space-between;
|
|
102
|
+
align-items: center;
|
|
103
|
+
gap: var(--space-3);
|
|
104
|
+
|
|
105
|
+
h3 {
|
|
106
|
+
font-size: var(--text-xl);
|
|
107
|
+
font-weight: var(--font-weight-semibold);
|
|
108
|
+
color: var(--text-primary);
|
|
109
|
+
margin: 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.page-permissions__detail-key {
|
|
114
|
+
font-family: monospace;
|
|
115
|
+
font-size: var(--text-xs);
|
|
116
|
+
color: var(--text-tertiary);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.page-permissions__detail-section {
|
|
120
|
+
gap: var(--space-3);
|
|
121
|
+
align-items: stretch;
|
|
122
|
+
|
|
123
|
+
h4 {
|
|
124
|
+
font-size: var(--text-base);
|
|
125
|
+
font-weight: var(--font-weight-medium);
|
|
126
|
+
color: var(--text-secondary);
|
|
127
|
+
margin: 0;
|
|
128
|
+
padding-bottom: var(--space-1);
|
|
129
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Debug panel (right) ──
|
|
134
|
+
|
|
135
|
+
.page-permissions__debug-panel {
|
|
136
|
+
width: 320px;
|
|
137
|
+
min-width: 320px;
|
|
138
|
+
border-left: 1px solid var(--border-default);
|
|
139
|
+
padding: var(--space-4);
|
|
140
|
+
gap: var(--space-3);
|
|
141
|
+
overflow-y: auto;
|
|
142
|
+
align-items: stretch;
|
|
143
|
+
background: var(--bg-secondary);
|
|
144
|
+
|
|
145
|
+
h4 {
|
|
146
|
+
font-size: var(--text-sm);
|
|
147
|
+
font-weight: var(--font-weight-semibold);
|
|
148
|
+
color: var(--text-secondary);
|
|
149
|
+
margin: 0;
|
|
150
|
+
text-transform: uppercase;
|
|
151
|
+
letter-spacing: 0.05em;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.page-permissions__debug-panel--empty {
|
|
156
|
+
background: var(--bg-secondary);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.page-permissions__debug-tree {
|
|
160
|
+
font-size: var(--text-xs);
|
|
161
|
+
overflow-x: auto;
|
|
162
|
+
|
|
163
|
+
.ts-json-viewer__item__key {
|
|
164
|
+
color: var(--text-secondary);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.ts-json-viewer__item__value {
|
|
168
|
+
color: var(--accent-primary);
|
|
169
|
+
word-break: break-all;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.ts-json-viewer__item.string .ts-json-viewer__item__value {
|
|
173
|
+
color: var(--status-active-fg);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.ts-json-viewer__item.number .ts-json-viewer__item__value {
|
|
177
|
+
color: var(--accent-primary);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Members ──
|
|
182
|
+
|
|
183
|
+
.page-permissions__member-editor {
|
|
184
|
+
gap: var(--space-3);
|
|
185
|
+
align-items: stretch;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.page-permissions__member-dropdown {
|
|
189
|
+
max-width: 300px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.page-permissions {
|
|
193
|
+
.tag--accent {
|
|
194
|
+
background: var(--accent-subtle);
|
|
195
|
+
color: var(--accent-primary);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.tag__remove {
|
|
199
|
+
background: none;
|
|
200
|
+
border: none;
|
|
201
|
+
cursor: pointer;
|
|
202
|
+
padding: 0 0 0 var(--space-1);
|
|
203
|
+
font-size: var(--text-sm);
|
|
204
|
+
color: inherit;
|
|
205
|
+
opacity: 0.6;
|
|
206
|
+
line-height: 1;
|
|
207
|
+
|
|
208
|
+
&:hover {
|
|
209
|
+
opacity: 1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.editor-panel__actions {
|
|
214
|
+
padding-top: var(--space-3);
|
|
215
|
+
border-top: 1px solid var(--border-subtle);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { PermissionScope } from '@nu-art/permissions-shared';
|
|
3
|
+
type Props = {
|
|
4
|
+
scope: PermissionScope;
|
|
5
|
+
value: string;
|
|
6
|
+
fallback?: React.ReactNode;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Conditionally renders children based on the current user's scope permissions.
|
|
11
|
+
* Reads scopeEntries from the JWT session — same PermissionScope used on backend.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* <PermissionGuard scope={PermissionScope_MyConcept} value="write">
|
|
15
|
+
* <MyProtectedComponent />
|
|
16
|
+
* </PermissionGuard>
|
|
17
|
+
*/
|
|
18
|
+
export declare const PermissionGuard: React.FC<Props>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ModuleFE_PermissionsAssert } from '../modules/ModuleFE_PermissionsAssert.js';
|
|
3
|
+
/**
|
|
4
|
+
* Conditionally renders children based on the current user's scope permissions.
|
|
5
|
+
* Reads scopeEntries from the JWT session — same PermissionScope used on backend.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* <PermissionGuard scope={PermissionScope_MyConcept} value="write">
|
|
9
|
+
* <MyProtectedComponent />
|
|
10
|
+
* </PermissionGuard>
|
|
11
|
+
*/
|
|
12
|
+
export const PermissionGuard = ({ scope, value, fallback, children }) => {
|
|
13
|
+
if (!ModuleFE_PermissionsAssert.hasScopeAccess(scope, value))
|
|
14
|
+
return _jsx(_Fragment, { children: fallback ?? null });
|
|
15
|
+
return _jsx(_Fragment, { children: children });
|
|
16
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DatabaseDef_PermissionScope } from '@nu-art/permissions-shared';
|
|
2
|
+
export type Props_ScopeLabels = {
|
|
3
|
+
scopeEntries: DatabaseDef_PermissionScope['id'][];
|
|
4
|
+
emptyMessage?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const Component_ScopeLabels: (props: Props_ScopeLabels) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { deriveScopeStructure, resolveScopeSelections } from './scope-utils.js';
|
|
3
|
+
export const Component_ScopeLabels = (props) => {
|
|
4
|
+
const scopes = deriveScopeStructure();
|
|
5
|
+
const selections = resolveScopeSelections(props.scopeEntries, scopes);
|
|
6
|
+
const labels = scopes
|
|
7
|
+
.filter(s => selections[s.key])
|
|
8
|
+
.map(s => ({ key: s.key, value: selections[s.key] }));
|
|
9
|
+
if (labels.length === 0)
|
|
10
|
+
return _jsx("div", { className: 'card-list__item-meta', children: props.emptyMessage ?? 'No scopes' });
|
|
11
|
+
return _jsx("div", { className: 'tags', children: labels.map(s => _jsxs("span", { className: 'tag', children: [s.key, ": ", s.value] }, s.key)) });
|
|
12
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ApiCallerEventType } from '@nu-art/db-api-shared';
|
|
2
|
+
import type { DB_PermissionScope, DatabaseDef_PermissionScope } from '@nu-art/permissions-shared';
|
|
3
|
+
import { ComponentSync } from '@nu-art/thunder-widgets';
|
|
4
|
+
import { type OnPermissionScopeUpdated } from '../../_entity/permission-scope/ModuleFE_PermissionScope.js';
|
|
5
|
+
import './Component_ScopeMultiSelect.scss';
|
|
6
|
+
export type Props_ScopeMultiSelect = {
|
|
7
|
+
scopeEntries: DatabaseDef_PermissionScope['id'][];
|
|
8
|
+
onChanged: (entries: DatabaseDef_PermissionScope['id'][]) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
};
|
|
11
|
+
type State = {
|
|
12
|
+
pickerOpen: boolean;
|
|
13
|
+
searchText: string;
|
|
14
|
+
activeDomains: Set<string>;
|
|
15
|
+
};
|
|
16
|
+
export declare class Component_ScopeMultiSelect extends ComponentSync<Props_ScopeMultiSelect, State> implements OnPermissionScopeUpdated {
|
|
17
|
+
__onPermissionScopeUpdated(..._params: ApiCallerEventType<DB_PermissionScope>): void;
|
|
18
|
+
protected deriveStateFromProps(_nextProps: Props_ScopeMultiSelect, state: State): State;
|
|
19
|
+
private readonly removeScope;
|
|
20
|
+
private readonly addScope;
|
|
21
|
+
private readonly toggleDomain;
|
|
22
|
+
render(): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
private renderSelectedChips;
|
|
24
|
+
private renderPicker;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { sortArray } from '@nu-art/ts-common';
|
|
3
|
+
import { ComponentSync, LL_H_C, LL_V_L, TS_Input } from '@nu-art/thunder-widgets';
|
|
4
|
+
import { ModuleFE_PermissionScope } from '../../_entity/permission-scope/ModuleFE_PermissionScope.js';
|
|
5
|
+
import './Component_ScopeMultiSelect.scss';
|
|
6
|
+
function firstSegment(s) {
|
|
7
|
+
const idx = s.indexOf('-');
|
|
8
|
+
return idx >= 0 ? s.slice(0, idx) : s;
|
|
9
|
+
}
|
|
10
|
+
function commonPrefixLength(a, b) {
|
|
11
|
+
let i = 0;
|
|
12
|
+
while (i < a.length && i < b.length && a[i] === b[i])
|
|
13
|
+
i++;
|
|
14
|
+
return i;
|
|
15
|
+
}
|
|
16
|
+
function deriveDomain(key, uiDomains) {
|
|
17
|
+
const keyFirstSeg = firstSegment(key);
|
|
18
|
+
let bestDomainSeg = '';
|
|
19
|
+
let bestLen = 0;
|
|
20
|
+
for (const domain of uiDomains) {
|
|
21
|
+
const domainFirstSeg = firstSegment(domain);
|
|
22
|
+
const prefixLen = commonPrefixLength(key, domain);
|
|
23
|
+
const threshold = Math.min(keyFirstSeg.length, domainFirstSeg.length);
|
|
24
|
+
if (prefixLen >= threshold && threshold >= 3 && prefixLen > bestLen) {
|
|
25
|
+
bestLen = prefixLen;
|
|
26
|
+
bestDomainSeg = domainFirstSeg;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return bestDomainSeg || 'other';
|
|
30
|
+
}
|
|
31
|
+
function buildScopeRows(entities) {
|
|
32
|
+
const uiDomains = entities
|
|
33
|
+
.filter(e => e.key.endsWith('-ui'))
|
|
34
|
+
.map(e => e.key.slice(0, -3));
|
|
35
|
+
return sortArray(entities.map(entity => ({
|
|
36
|
+
entity,
|
|
37
|
+
label: `${entity.key}:${entity.value}`,
|
|
38
|
+
domain: deriveDomain(entity.key, uiDomains),
|
|
39
|
+
})), row => row.label);
|
|
40
|
+
}
|
|
41
|
+
export class Component_ScopeMultiSelect extends ComponentSync {
|
|
42
|
+
__onPermissionScopeUpdated(..._params) {
|
|
43
|
+
this.forceUpdate();
|
|
44
|
+
}
|
|
45
|
+
deriveStateFromProps(_nextProps, state) {
|
|
46
|
+
return {
|
|
47
|
+
pickerOpen: state?.pickerOpen ?? false,
|
|
48
|
+
searchText: state?.searchText ?? '',
|
|
49
|
+
activeDomains: state?.activeDomains ?? new Set(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
removeScope = (id) => {
|
|
53
|
+
if (this.props.disabled)
|
|
54
|
+
return;
|
|
55
|
+
const next = this.props.scopeEntries.filter(e => e !== id);
|
|
56
|
+
this.props.onChanged(next);
|
|
57
|
+
};
|
|
58
|
+
addScope = (entity) => {
|
|
59
|
+
if (this.props.disabled)
|
|
60
|
+
return;
|
|
61
|
+
const selectedSet = new Set(this.props.scopeEntries);
|
|
62
|
+
if (selectedSet.has(entity._id))
|
|
63
|
+
return;
|
|
64
|
+
this.props.onChanged([...this.props.scopeEntries, entity._id]);
|
|
65
|
+
};
|
|
66
|
+
toggleDomain = (domain) => {
|
|
67
|
+
const next = new Set(this.state.activeDomains);
|
|
68
|
+
if (next.has(domain))
|
|
69
|
+
next.delete(domain);
|
|
70
|
+
else
|
|
71
|
+
next.add(domain);
|
|
72
|
+
this.setState({ activeDomains: next });
|
|
73
|
+
};
|
|
74
|
+
render() {
|
|
75
|
+
const entities = ModuleFE_PermissionScope.cache.all();
|
|
76
|
+
const allRows = buildScopeRows(entities);
|
|
77
|
+
const selectedSet = new Set(this.props.scopeEntries);
|
|
78
|
+
const selectedRows = allRows.filter(r => selectedSet.has(r.entity._id));
|
|
79
|
+
return _jsxs(LL_V_L, { className: 'scope-multiselect', children: [this.renderSelectedChips(selectedRows), this.state.pickerOpen
|
|
80
|
+
? this.renderPicker(allRows, selectedSet)
|
|
81
|
+
: _jsx("button", { className: 'btn btn--secondary btn--sm scope-multiselect__add-btn', onClick: () => this.setState({ pickerOpen: true }), children: "+ Add scopes" })] });
|
|
82
|
+
}
|
|
83
|
+
renderSelectedChips(selectedRows) {
|
|
84
|
+
if (selectedRows.length === 0)
|
|
85
|
+
return _jsx("span", { className: 'card-list__item-meta', children: "No scopes assigned" });
|
|
86
|
+
return _jsx("div", { className: 'scope-multiselect__chips', children: selectedRows.map(row => (_jsxs("span", { className: 'scope-multiselect__chip', children: [_jsxs("span", { className: 'scope-multiselect__chip-label', children: [row.entity.key, ":", row.entity.value] }), !this.props.disabled && _jsx("button", { className: 'scope-multiselect__chip-remove', onClick: () => this.removeScope(row.entity._id), children: "\u00D7" })] }, row.entity._id))) });
|
|
87
|
+
}
|
|
88
|
+
renderPicker(allRows, selectedSet) {
|
|
89
|
+
const domains = sortArray([...new Set(allRows.map(r => r.domain))], d => d);
|
|
90
|
+
const availableRows = allRows.filter(row => {
|
|
91
|
+
if (selectedSet.has(row.entity._id))
|
|
92
|
+
return false;
|
|
93
|
+
if (this.state.activeDomains.size > 0 && !this.state.activeDomains.has(row.domain))
|
|
94
|
+
return false;
|
|
95
|
+
if (this.state.searchText) {
|
|
96
|
+
const needle = this.state.searchText.toLowerCase();
|
|
97
|
+
if (!row.label.toLowerCase().includes(needle))
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
102
|
+
return _jsxs(LL_V_L, { className: 'scope-multiselect__picker', children: [_jsxs(LL_H_C, { className: 'scope-multiselect__picker-header', children: [_jsx(TS_Input, { type: 'text', value: this.state.searchText, placeholder: 'Search scopes to add...', saveEvent: ['change'], onChange: value => this.setState({ searchText: value }), focus: true }), _jsx("button", { className: 'btn btn--ghost btn--sm', onClick: () => this.setState({ pickerOpen: false, searchText: '', activeDomains: new Set() }), children: "Done" })] }), _jsx(LL_H_C, { className: 'scope-multiselect__domain-bar', children: domains.map(domain => {
|
|
103
|
+
const isActive = this.state.activeDomains.has(domain);
|
|
104
|
+
return _jsx("button", { className: `scope-multiselect__domain-pill ${isActive ? 'scope-multiselect__domain-pill--active' : ''}`, onClick: () => this.toggleDomain(domain), children: domain }, domain);
|
|
105
|
+
}) }), _jsx(LL_V_L, { className: 'scope-multiselect__picker-list', children: availableRows.length === 0
|
|
106
|
+
? _jsx("span", { className: 'scope-multiselect__picker-empty', children: "No matching scopes" })
|
|
107
|
+
: availableRows.map(row => (_jsxs(LL_H_C, { className: 'scope-multiselect__picker-row', onClick: () => this.addScope(row.entity), children: [_jsx("span", { className: 'scope-multiselect__picker-row-key', children: row.entity.key }), _jsx("span", { className: 'scope-multiselect__picker-row-sep', children: ":" }), _jsx("span", { className: 'scope-multiselect__picker-row-value', children: row.entity.value }), _jsx("span", { className: 'badge badge--info scope-multiselect__picker-row-domain', children: row.domain })] }, row.entity._id))) })] });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
.scope-multiselect {
|
|
2
|
+
gap: var(--space-3);
|
|
3
|
+
align-items: stretch;
|
|
4
|
+
width: 100%;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// ── Selected chips ──
|
|
8
|
+
|
|
9
|
+
.scope-multiselect__chips {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-wrap: wrap;
|
|
12
|
+
gap: var(--space-1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.scope-multiselect__chip {
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: var(--space-1);
|
|
19
|
+
padding: 2px var(--space-2);
|
|
20
|
+
border-radius: var(--radius-sm);
|
|
21
|
+
font-size: var(--text-xs);
|
|
22
|
+
font-weight: var(--font-weight-medium);
|
|
23
|
+
background: var(--accent-subtle);
|
|
24
|
+
color: var(--accent-primary);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.scope-multiselect__chip-remove {
|
|
28
|
+
background: none;
|
|
29
|
+
border: none;
|
|
30
|
+
cursor: pointer;
|
|
31
|
+
padding: 0;
|
|
32
|
+
font-size: var(--text-sm);
|
|
33
|
+
color: inherit;
|
|
34
|
+
opacity: 0.6;
|
|
35
|
+
line-height: 1;
|
|
36
|
+
|
|
37
|
+
&:hover {
|
|
38
|
+
opacity: 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.scope-multiselect__add-btn {
|
|
43
|
+
align-self: flex-start;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Picker (dropdown-like panel) ──
|
|
47
|
+
|
|
48
|
+
.scope-multiselect__picker {
|
|
49
|
+
gap: var(--space-2);
|
|
50
|
+
align-items: stretch;
|
|
51
|
+
border: 1px solid var(--border-default);
|
|
52
|
+
border-radius: var(--radius-md);
|
|
53
|
+
padding: var(--space-3);
|
|
54
|
+
background: var(--bg-elevated);
|
|
55
|
+
box-shadow: var(--shadow-sm);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.scope-multiselect__picker-header {
|
|
59
|
+
gap: var(--space-2);
|
|
60
|
+
align-items: center;
|
|
61
|
+
|
|
62
|
+
.ts-input {
|
|
63
|
+
flex: 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.scope-multiselect__domain-bar {
|
|
68
|
+
gap: var(--space-1);
|
|
69
|
+
flex-wrap: wrap;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.scope-multiselect__domain-pill {
|
|
73
|
+
display: inline-flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
padding: 2px var(--space-2);
|
|
76
|
+
border-radius: var(--radius-full);
|
|
77
|
+
font-size: var(--text-xs);
|
|
78
|
+
font-weight: var(--font-weight-medium);
|
|
79
|
+
border: 1px solid var(--border-default);
|
|
80
|
+
background: var(--bg-secondary);
|
|
81
|
+
color: var(--text-secondary);
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
transition: background var(--transition-fast), color var(--transition-fast),
|
|
84
|
+
border-color var(--transition-fast);
|
|
85
|
+
|
|
86
|
+
&:hover {
|
|
87
|
+
background: var(--bg-tertiary);
|
|
88
|
+
color: var(--text-primary);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.scope-multiselect__domain-pill--active {
|
|
93
|
+
background: var(--accent-subtle);
|
|
94
|
+
color: var(--accent-primary);
|
|
95
|
+
border-color: var(--accent-primary);
|
|
96
|
+
|
|
97
|
+
&:hover {
|
|
98
|
+
background: var(--accent-subtle);
|
|
99
|
+
color: var(--accent-primary);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.scope-multiselect__picker-list {
|
|
104
|
+
gap: 1px;
|
|
105
|
+
align-items: stretch;
|
|
106
|
+
max-height: 280px;
|
|
107
|
+
overflow-y: auto;
|
|
108
|
+
border: 1px solid var(--border-subtle);
|
|
109
|
+
border-radius: var(--radius-sm);
|
|
110
|
+
background: var(--border-subtle);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.scope-multiselect__picker-row {
|
|
114
|
+
gap: var(--space-2);
|
|
115
|
+
align-items: center;
|
|
116
|
+
padding: var(--space-2) var(--space-3);
|
|
117
|
+
background: var(--bg-primary);
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
transition: background var(--transition-fast);
|
|
120
|
+
|
|
121
|
+
&:hover {
|
|
122
|
+
background: var(--accent-subtle);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.scope-multiselect__picker-row-key {
|
|
127
|
+
font-size: var(--text-sm);
|
|
128
|
+
font-weight: var(--font-weight-medium);
|
|
129
|
+
color: var(--text-primary);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.scope-multiselect__picker-row-sep {
|
|
133
|
+
font-size: var(--text-sm);
|
|
134
|
+
color: var(--text-tertiary);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.scope-multiselect__picker-row-value {
|
|
138
|
+
font-size: var(--text-sm);
|
|
139
|
+
color: var(--text-secondary);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.scope-multiselect__picker-row-domain {
|
|
143
|
+
margin-left: auto;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.scope-multiselect__picker-empty {
|
|
147
|
+
font-size: var(--text-sm);
|
|
148
|
+
color: var(--text-tertiary);
|
|
149
|
+
padding: var(--space-3);
|
|
150
|
+
text-align: center;
|
|
151
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DatabaseDef_PermissionScope, DB_PermissionScope } from '@nu-art/permissions-shared';
|
|
2
|
+
export type ScopeDescriptor = {
|
|
3
|
+
key: string;
|
|
4
|
+
values: string[];
|
|
5
|
+
};
|
|
6
|
+
export declare function deriveScopeStructure(): ScopeDescriptor[];
|
|
7
|
+
export declare function resolveScopeSelections(scopeEntries: DatabaseDef_PermissionScope['id'][], scopes?: ScopeDescriptor[], scopeEntities?: readonly Readonly<DB_PermissionScope>[]): Record<string, string>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Scope derivation and resolution utilities.
|
|
3
|
+
* Used by Component_ScopeLabels and other read-only scope display.
|
|
4
|
+
* Copyright (C) 2026 Adam van der Kruk aka TacB0sS
|
|
5
|
+
* Licensed under the Apache License, Version 2.0
|
|
6
|
+
*/
|
|
7
|
+
import { sortArray } from '@nu-art/ts-common';
|
|
8
|
+
import { getPermissionScopeValues } from '@nu-art/permissions-shared';
|
|
9
|
+
import { ModuleFE_PermissionScope } from '../../_entity/permission-scope/ModuleFE_PermissionScope.js';
|
|
10
|
+
export function deriveScopeStructure() {
|
|
11
|
+
const entities = ModuleFE_PermissionScope.cache.all();
|
|
12
|
+
const scopeKeys = new Set(entities.map(e => e.key));
|
|
13
|
+
return sortArray([...scopeKeys].map(key => {
|
|
14
|
+
const definedValues = getPermissionScopeValues(key);
|
|
15
|
+
if (definedValues)
|
|
16
|
+
return { key, values: [...definedValues] };
|
|
17
|
+
return { key, values: entities.filter(e => e.key === key).map(e => e.value) };
|
|
18
|
+
}), d => d.key);
|
|
19
|
+
}
|
|
20
|
+
export function resolveScopeSelections(scopeEntries, scopes, scopeEntities) {
|
|
21
|
+
const resolvedScopes = scopes ?? deriveScopeStructure();
|
|
22
|
+
const resolvedEntities = scopeEntities ?? ModuleFE_PermissionScope.cache.all();
|
|
23
|
+
const entryIds = new Set(scopeEntries);
|
|
24
|
+
const selections = {};
|
|
25
|
+
for (const scope of resolvedScopes) {
|
|
26
|
+
let maxIdx = -1;
|
|
27
|
+
for (const entity of resolvedEntities) {
|
|
28
|
+
if (entity.key !== scope.key || !entryIds.has(entity._id))
|
|
29
|
+
continue;
|
|
30
|
+
const idx = scope.values.indexOf(entity.value);
|
|
31
|
+
if (idx > maxIdx)
|
|
32
|
+
maxIdx = idx;
|
|
33
|
+
}
|
|
34
|
+
if (maxIdx >= 0)
|
|
35
|
+
selections[scope.key] = scope.values[maxIdx];
|
|
36
|
+
}
|
|
37
|
+
return selections;
|
|
38
|
+
}
|
package/PermissionKey_FE.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { AppConfigKey_FE } from '@nu-art/thunderstorm-frontend/index';
|
|
2
|
-
import { TypedKeyValue, UniqueId } from '@nu-art/ts-common';
|
|
3
|
-
import { DB_PermissionKeyData, DomainToLevelValueMap, PermissionKey } from '@nu-art/permissions-shared';
|
|
4
|
-
import { AccessLevel } from './modules/ModuleFE_PermissionsAssert.js';
|
|
5
|
-
export type UI_PermissionKeyData = {
|
|
6
|
-
accessLevelIds: UniqueId[];
|
|
7
|
-
_accessLevels?: DomainToLevelValueMap;
|
|
8
|
-
};
|
|
9
|
-
export declare class PermissionKey_FE<K extends string = string> extends AppConfigKey_FE<TypedKeyValue<K, DB_PermissionKeyData>> {
|
|
10
|
-
static generatePermissionKeysByLevels: <K_ extends PermissionKey>(keysMapper: { [key in K_]: string; }) => { [key in K_]: PermissionKey_FE; };
|
|
11
|
-
constructor(key: K);
|
|
12
|
-
set(value: UI_PermissionKeyData): Promise<void>;
|
|
13
|
-
getAccessLevel(): AccessLevel;
|
|
14
|
-
}
|
|
15
|
-
export type ModuleFE_PermissionMapper<Mapper> = {
|
|
16
|
-
[key in keyof Mapper]: PermissionKey_FE;
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Permission mapper type for ModuleFEs
|
|
20
|
-
*/
|
|
21
|
-
export type ModuleFE_DefaultPermissions<UIMapper, CollectionMapper> = {
|
|
22
|
-
ui: ModuleFE_PermissionMapper<UIMapper>;
|
|
23
|
-
collection: ModuleFE_PermissionMapper<CollectionMapper>;
|
|
24
|
-
};
|
package/PermissionKey_FE.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { AppConfigKey_FE, ModuleFE_AppConfig } from '@nu-art/thunderstorm-frontend/index';
|
|
2
|
-
import { _keys } from '@nu-art/ts-common';
|
|
3
|
-
import { ModuleFE_PermissionsAssert } from './modules/ModuleFE_PermissionsAssert.js';
|
|
4
|
-
export class PermissionKey_FE extends AppConfigKey_FE {
|
|
5
|
-
static generatePermissionKeysByLevels = (keysMapper) => {
|
|
6
|
-
return _keys(keysMapper).reduce((mapper, currentKey) => {
|
|
7
|
-
if (!mapper[currentKey])
|
|
8
|
-
mapper[currentKey] = new PermissionKey_FE(keysMapper[currentKey]);
|
|
9
|
-
return mapper;
|
|
10
|
-
}, {});
|
|
11
|
-
};
|
|
12
|
-
constructor(key) {
|
|
13
|
-
super(key);
|
|
14
|
-
ModuleFE_PermissionsAssert.registerPermissionKey(this);
|
|
15
|
-
}
|
|
16
|
-
async set(value) {
|
|
17
|
-
// @ts-ignore
|
|
18
|
-
await ModuleFE_AppConfig.set(this, value);
|
|
19
|
-
}
|
|
20
|
-
getAccessLevel() {
|
|
21
|
-
return ModuleFE_PermissionsAssert.getAccessLevel(this);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { ModuleFE_BaseApi } from '@nu-art/thunderstorm-frontend';
|
|
2
|
-
import { ApiDefCaller } from '@nu-art/thunderstorm-shared';
|
|
3
|
-
import { DispatcherDef, ThunderDispatcherV3 } from '@nu-art/thunderstorm-frontend/core/db-api-gen/types';
|
|
4
|
-
import { ApiStruct_PermissionAccessLevel, DBProto_PermissionAccessLevel } from '@nu-art/permissions-shared';
|
|
5
|
-
export type DispatcherType_PermissionAccessLevel = DispatcherDef<DBProto_PermissionAccessLevel, `__onPermissionAccessLevelUpdated`>;
|
|
6
|
-
export declare const dispatch_onPermissionAccessLevelChanged: ThunderDispatcherV3<DispatcherType_PermissionAccessLevel>;
|
|
7
|
-
export declare class ModuleFE_PermissionAccessLevel_Class extends ModuleFE_BaseApi<DBProto_PermissionAccessLevel> implements ApiDefCaller<ApiStruct_PermissionAccessLevel> {
|
|
8
|
-
_v1: ApiDefCaller<ApiStruct_PermissionAccessLevel>['_v1'];
|
|
9
|
-
constructor();
|
|
10
|
-
}
|
|
11
|
-
export declare const ModuleFE_PermissionAccessLevel: ModuleFE_PermissionAccessLevel_Class;
|