@byline/admin 2.3.3 → 2.4.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/dist/abilities.js +5 -24
- package/dist/index.js +8 -30
- package/dist/lib/assert-admin-actor.js +13 -74
- package/dist/lib/create-command.js +6 -16
- package/dist/modules/admin-account/commands.js +35 -24
- package/dist/modules/admin-account/components/change-password.d.ts +8 -0
- package/dist/modules/admin-account/components/change-password.js +192 -0
- package/dist/modules/admin-account/components/change-password.module.js +8 -0
- package/dist/modules/admin-account/components/change-password_module.css +27 -0
- package/dist/modules/admin-account/components/container.d.ts +29 -0
- package/dist/modules/admin-account/components/container.js +298 -0
- package/dist/modules/admin-account/components/container.module.js +28 -0
- package/dist/modules/admin-account/components/container_module.css +106 -0
- package/dist/modules/admin-account/components/update.d.ts +8 -0
- package/dist/modules/admin-account/components/update.js +207 -0
- package/dist/modules/admin-account/components/update.module.js +8 -0
- package/dist/modules/admin-account/components/update_module.css +27 -0
- package/dist/modules/admin-account/errors.js +14 -45
- package/dist/modules/admin-account/index.js +4 -34
- package/dist/modules/admin-account/schemas.js +25 -59
- package/dist/modules/admin-account/service.js +56 -61
- package/dist/modules/admin-permissions/abilities.js +6 -24
- package/dist/modules/admin-permissions/commands.js +42 -28
- package/dist/modules/admin-permissions/components/inspector.d.ts +4 -0
- package/dist/modules/admin-permissions/components/inspector.js +284 -0
- package/dist/modules/admin-permissions/components/inspector.module.js +56 -0
- package/dist/modules/admin-permissions/components/inspector_module.css +238 -0
- package/dist/modules/admin-permissions/dto.js +3 -16
- package/dist/modules/admin-permissions/errors.js +14 -27
- package/dist/modules/admin-permissions/index.js +6 -26
- package/dist/modules/admin-permissions/repository.js +1 -8
- package/dist/modules/admin-permissions/schemas.js +33 -70
- package/dist/modules/admin-permissions/service.js +88 -92
- package/dist/modules/admin-roles/abilities.js +8 -30
- package/dist/modules/admin-roles/commands.js +89 -55
- package/dist/modules/admin-roles/components/create.d.ts +7 -0
- package/dist/modules/admin-roles/components/create.js +177 -0
- package/dist/modules/admin-roles/components/create.module.js +8 -0
- package/dist/modules/admin-roles/components/create_module.css +27 -0
- package/dist/modules/admin-roles/components/permissions.d.ts +10 -0
- package/dist/modules/admin-roles/components/permissions.js +303 -0
- package/dist/modules/admin-roles/components/permissions.module.js +44 -0
- package/dist/modules/admin-roles/components/permissions_module.css +192 -0
- package/dist/modules/admin-roles/components/update.d.ts +8 -0
- package/dist/modules/admin-roles/components/update.js +166 -0
- package/dist/modules/admin-roles/components/update.module.js +8 -0
- package/dist/modules/admin-roles/components/update_module.css +27 -0
- package/dist/modules/admin-roles/dto.js +3 -16
- package/dist/modules/admin-roles/errors.js +16 -40
- package/dist/modules/admin-roles/index.js +6 -26
- package/dist/modules/admin-roles/repository.js +1 -8
- package/dist/modules/admin-roles/schemas.js +41 -71
- package/dist/modules/admin-roles/service.js +79 -82
- package/dist/modules/admin-users/abilities.js +9 -38
- package/dist/modules/admin-users/commands.js +92 -50
- package/dist/modules/admin-users/components/create.d.ts +8 -0
- package/dist/modules/admin-users/components/create.js +268 -0
- package/dist/modules/admin-users/components/create.module.js +10 -0
- package/dist/modules/admin-users/components/create_module.css +45 -0
- package/dist/modules/admin-users/components/roles.d.ts +11 -0
- package/dist/modules/admin-users/components/roles.js +148 -0
- package/dist/modules/admin-users/components/roles.module.js +18 -0
- package/dist/modules/admin-users/components/roles_module.css +75 -0
- package/dist/modules/admin-users/components/set-password.d.ts +8 -0
- package/dist/modules/admin-users/components/set-password.js +170 -0
- package/dist/modules/admin-users/components/set-password.module.js +9 -0
- package/dist/modules/admin-users/components/set-password_module.css +31 -0
- package/dist/modules/admin-users/components/update.d.ts +8 -0
- package/dist/modules/admin-users/components/update.js +254 -0
- package/dist/modules/admin-users/components/update.module.js +9 -0
- package/dist/modules/admin-users/components/update_module.css +34 -0
- package/dist/modules/admin-users/dto.js +3 -18
- package/dist/modules/admin-users/errors.js +17 -43
- package/dist/modules/admin-users/index.js +7 -27
- package/dist/modules/admin-users/repository.js +1 -8
- package/dist/modules/admin-users/schemas.js +44 -75
- package/dist/modules/admin-users/seed-super-admin.js +9 -34
- package/dist/modules/admin-users/service.js +76 -91
- package/dist/modules/auth/components/sign-in-form.d.ts +12 -0
- package/dist/modules/auth/components/sign-in-form.js +115 -0
- package/dist/modules/auth/components/sign-in-form.module.js +12 -0
- package/dist/modules/auth/components/sign-in-form_module.css +41 -0
- package/dist/modules/auth/index.js +3 -24
- package/dist/modules/auth/jwt-session-provider.js +179 -149
- package/dist/modules/auth/password.js +11 -53
- package/dist/modules/auth/phc.js +21 -54
- package/dist/modules/auth/refresh-tokens-repository.js +1 -8
- package/dist/modules/auth/resolve-actor.js +6 -28
- package/dist/services/admin-services-context.d.ts +16 -0
- package/dist/services/admin-services-context.js +13 -0
- package/dist/services/admin-services-types.d.ts +129 -0
- package/dist/services/admin-services-types.js +1 -0
- package/dist/store.js +1 -8
- package/dist/vendor/noble-argon2/_blake.js +277 -45
- package/dist/vendor/noble-argon2/_md.js +81 -136
- package/dist/vendor/noble-argon2/_u64.js +65 -67
- package/dist/vendor/noble-argon2/argon2.js +181 -342
- package/dist/vendor/noble-argon2/blake2.js +252 -327
- package/dist/vendor/noble-argon2/utils.js +110 -490
- package/dist/vendor/noble-argon2/utils.js.LICENSE.txt +1 -0
- package/package.json +89 -10
- package/src/abilities.ts +32 -0
- package/src/declarations.d.ts +4 -0
- package/src/index.ts +39 -0
- package/src/lib/assert-admin-actor.ts +90 -0
- package/src/lib/create-command.ts +109 -0
- package/src/modules/admin-account/commands.ts +76 -0
- package/src/modules/admin-account/components/change-password.module.css +40 -0
- package/src/modules/admin-account/components/change-password.tsx +232 -0
- package/src/modules/admin-account/components/container.module.css +158 -0
- package/src/modules/admin-account/components/container.tsx +229 -0
- package/src/modules/admin-account/components/update.module.css +40 -0
- package/src/modules/admin-account/components/update.tsx +263 -0
- package/src/modules/admin-account/errors.ts +75 -0
- package/src/modules/admin-account/index.ts +60 -0
- package/src/modules/admin-account/schemas.ts +84 -0
- package/src/modules/admin-account/service.ts +92 -0
- package/src/modules/admin-permissions/abilities.ts +46 -0
- package/src/modules/admin-permissions/commands.ts +103 -0
- package/src/modules/admin-permissions/components/inspector.module.css +326 -0
- package/src/modules/admin-permissions/components/inspector.tsx +298 -0
- package/src/modules/admin-permissions/dto.ts +28 -0
- package/src/modules/admin-permissions/errors.ts +57 -0
- package/src/modules/admin-permissions/index.ts +72 -0
- package/src/modules/admin-permissions/repository.ts +49 -0
- package/src/modules/admin-permissions/schemas.ts +128 -0
- package/src/modules/admin-permissions/service.ts +137 -0
- package/src/modules/admin-roles/abilities.ts +62 -0
- package/src/modules/admin-roles/commands.ts +161 -0
- package/src/modules/admin-roles/components/create.module.css +40 -0
- package/src/modules/admin-roles/components/create.tsx +218 -0
- package/src/modules/admin-roles/components/permissions.module.css +279 -0
- package/src/modules/admin-roles/components/permissions.tsx +396 -0
- package/src/modules/admin-roles/components/update.module.css +40 -0
- package/src/modules/admin-roles/components/update.tsx +218 -0
- package/src/modules/admin-roles/dto.ts +30 -0
- package/src/modules/admin-roles/errors.ts +76 -0
- package/src/modules/admin-roles/index.ts +81 -0
- package/src/modules/admin-roles/repository.ts +96 -0
- package/src/modules/admin-roles/schemas.ts +139 -0
- package/src/modules/admin-roles/service.ts +136 -0
- package/src/modules/admin-users/abilities.ts +76 -0
- package/src/modules/admin-users/commands.ts +157 -0
- package/src/modules/admin-users/components/create.module.css +63 -0
- package/src/modules/admin-users/components/create.tsx +323 -0
- package/src/modules/admin-users/components/roles.module.css +119 -0
- package/src/modules/admin-users/components/roles.tsx +172 -0
- package/src/modules/admin-users/components/set-password.module.css +46 -0
- package/src/modules/admin-users/components/set-password.tsx +199 -0
- package/src/modules/admin-users/components/update.module.css +49 -0
- package/src/modules/admin-users/components/update.tsx +328 -0
- package/src/modules/admin-users/dto.ts +39 -0
- package/src/modules/admin-users/errors.ts +84 -0
- package/src/modules/admin-users/index.ts +91 -0
- package/src/modules/admin-users/repository.ts +161 -0
- package/src/modules/admin-users/schemas.ts +168 -0
- package/src/modules/admin-users/seed-super-admin.ts +102 -0
- package/src/modules/admin-users/service.ts +166 -0
- package/src/modules/auth/components/sign-in-form.module.css +62 -0
- package/src/modules/auth/components/sign-in-form.tsx +132 -0
- package/src/modules/auth/index.ts +31 -0
- package/src/modules/auth/jwt-session-provider.ts +301 -0
- package/src/modules/auth/password.ts +94 -0
- package/src/modules/auth/phc.ts +121 -0
- package/src/modules/auth/refresh-tokens-repository.ts +74 -0
- package/src/modules/auth/resolve-actor.ts +42 -0
- package/src/services/admin-services-context.tsx +52 -0
- package/src/services/admin-services-types.ts +177 -0
- package/src/store.ts +32 -0
- package/src/vendor/noble-argon2/LICENSE +21 -0
- package/src/vendor/noble-argon2/README.md +87 -0
- package/src/vendor/noble-argon2/_blake.ts +58 -0
- package/src/vendor/noble-argon2/_md.ts +223 -0
- package/src/vendor/noble-argon2/_u64.ts +118 -0
- package/src/vendor/noble-argon2/argon2.ts +668 -0
- package/src/vendor/noble-argon2/blake2.ts +583 -0
- package/src/vendor/noble-argon2/utils.ts +849 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RolePermissions — inline per-role ability editor.
|
|
3
|
+
*
|
|
4
|
+
* Override handles:
|
|
5
|
+
* .byline-role-permissions-wrap — outer column container
|
|
6
|
+
* .byline-role-permissions-toolbar — top toolbar (mode toggle + count + actions)
|
|
7
|
+
* .byline-role-permissions-counter — "n of m selected/granted" line
|
|
8
|
+
* .byline-role-permissions-counter-num — bold number span
|
|
9
|
+
* .byline-role-permissions-actions — Cancel/Save cluster (right-aligned)
|
|
10
|
+
* .byline-role-permissions-action — Cancel/Save buttons
|
|
11
|
+
* .byline-role-permissions-groups — vertical group stack
|
|
12
|
+
* .byline-role-permissions-group — group card
|
|
13
|
+
* .byline-role-permissions-group-head — group header row
|
|
14
|
+
* .byline-role-permissions-group-title — group name + count line
|
|
15
|
+
* .byline-role-permissions-group-name — group display name
|
|
16
|
+
* .byline-role-permissions-group-count — n-of-m count text
|
|
17
|
+
* .byline-role-permissions-group-buttons — Select-all/Clear cluster
|
|
18
|
+
* .byline-role-permissions-grid — ability checkbox grid
|
|
19
|
+
* .byline-role-permissions-row — single ability row
|
|
20
|
+
* .byline-role-permissions-label — clickable label
|
|
21
|
+
* .byline-role-permissions-label-edit — edit-mode cursor variant
|
|
22
|
+
* .byline-role-permissions-label-head — name + key pill row
|
|
23
|
+
* .byline-role-permissions-label-name — ability display name
|
|
24
|
+
* .byline-role-permissions-key — code pill (ability key)
|
|
25
|
+
* .byline-role-permissions-description — ability description
|
|
26
|
+
* .byline-role-permissions-mode-toggle — segmented View/Edit toggle
|
|
27
|
+
* .byline-role-permissions-mode-button — toggle button base
|
|
28
|
+
* .byline-role-permissions-mode-button-active — active toggle button
|
|
29
|
+
* .byline-role-permissions-mode-button-divider — left-border separator on the second button
|
|
30
|
+
* .byline-role-permissions-mode-button-disabled — disabled-cursor modifier
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
.wrap,
|
|
34
|
+
:global(.byline-role-permissions-wrap) {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
gap: var(--spacing-12);
|
|
38
|
+
margin-bottom: var(--spacing-48);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.toolbar,
|
|
42
|
+
:global(.byline-role-permissions-toolbar) {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-wrap: wrap;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: var(--spacing-12);
|
|
47
|
+
padding: var(--spacing-12);
|
|
48
|
+
border: var(--border-width-thin) var(--border-style-solid) var(--gray-100);
|
|
49
|
+
background-color: var(--canvas-25);
|
|
50
|
+
border-radius: var(--border-radius-sm);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.counter,
|
|
54
|
+
:global(.byline-role-permissions-counter) {
|
|
55
|
+
margin: 0;
|
|
56
|
+
font-size: var(--font-size-sm);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.counter-num,
|
|
60
|
+
:global(.byline-role-permissions-counter-num) {
|
|
61
|
+
font-weight: var(--font-weight-medium);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.actions,
|
|
65
|
+
:global(.byline-role-permissions-actions) {
|
|
66
|
+
margin-left: auto;
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: var(--spacing-8);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.action,
|
|
73
|
+
:global(.byline-role-permissions-action) {
|
|
74
|
+
min-width: 4rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
* Checkbox shrink-fit — overrides the uikit Checkbox's `width: 100%`
|
|
79
|
+
* default container so the external label sits flush against the
|
|
80
|
+
* checkbox button. Replaces the `!w-auto` Tailwind utility we used
|
|
81
|
+
* pre-lift; with the component now in a published package, Tailwind's
|
|
82
|
+
* source scanner no longer sees that class string in the host so the
|
|
83
|
+
* rule was never generated and the label was being pushed to the right.
|
|
84
|
+
*
|
|
85
|
+
* `!important` is required — the uikit Checkbox sets `width: 100%`
|
|
86
|
+
* at the same specificity, so a plain rule loses to source-order. The
|
|
87
|
+
* pre-lift Tailwind class `!w-auto` carried `!important` via Tailwind's
|
|
88
|
+
* `!` prefix; this is the same effect, expressed in a CSS module.
|
|
89
|
+
*/
|
|
90
|
+
.checkbox-auto,
|
|
91
|
+
:global(.byline-role-permissions-checkbox-auto) {
|
|
92
|
+
/* biome-ignore lint/complexity/noImportantStyles: see comment above */
|
|
93
|
+
width: auto !important;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.groups,
|
|
97
|
+
:global(.byline-role-permissions-groups) {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: var(--spacing-8);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.group,
|
|
104
|
+
:global(.byline-role-permissions-group) {
|
|
105
|
+
border: var(--border-width-thin) var(--border-style-solid) var(--gray-100);
|
|
106
|
+
border-radius: var(--border-radius-sm);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.group-head,
|
|
110
|
+
:global(.byline-role-permissions-group-head) {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: space-between;
|
|
114
|
+
padding: var(--spacing-12);
|
|
115
|
+
border-bottom: var(--border-width-thin) var(--border-style-solid) var(--gray-100);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.group-name,
|
|
119
|
+
:global(.byline-role-permissions-group-name) {
|
|
120
|
+
font-weight: var(--font-weight-medium);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.group-count,
|
|
124
|
+
:global(.byline-role-permissions-group-count) {
|
|
125
|
+
margin-left: var(--spacing-8);
|
|
126
|
+
font-size: var(--font-size-xs);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.group-buttons,
|
|
130
|
+
:global(.byline-role-permissions-group-buttons) {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: var(--spacing-4);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.grid,
|
|
137
|
+
:global(.byline-role-permissions-grid) {
|
|
138
|
+
display: grid;
|
|
139
|
+
grid-template-columns: 1fr;
|
|
140
|
+
gap: var(--spacing-4);
|
|
141
|
+
padding: var(--spacing-12);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@media (min-width: 40rem) {
|
|
145
|
+
.grid,
|
|
146
|
+
:global(.byline-role-permissions-grid) {
|
|
147
|
+
grid-template-columns: 1fr 1fr;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.row,
|
|
152
|
+
:global(.byline-role-permissions-row) {
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: flex-start;
|
|
155
|
+
gap: var(--spacing-8);
|
|
156
|
+
padding: var(--spacing-4) 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.label,
|
|
160
|
+
:global(.byline-role-permissions-label) {
|
|
161
|
+
min-width: 0;
|
|
162
|
+
flex: 1 1 0;
|
|
163
|
+
cursor: default;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.label-edit,
|
|
167
|
+
:global(.byline-role-permissions-label-edit) {
|
|
168
|
+
cursor: pointer;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.label-head,
|
|
172
|
+
:global(.byline-role-permissions-label-head) {
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: var(--spacing-8);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.label-name,
|
|
179
|
+
:global(.byline-role-permissions-label-name) {
|
|
180
|
+
font-size: var(--font-size-sm);
|
|
181
|
+
font-weight: var(--font-weight-medium);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.key,
|
|
185
|
+
:global(.byline-role-permissions-key) {
|
|
186
|
+
background-color: var(--gray-100);
|
|
187
|
+
padding: 0.125rem 0.375rem;
|
|
188
|
+
font-size: var(--font-size-xs);
|
|
189
|
+
border-radius: var(--border-radius-sm);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.description,
|
|
193
|
+
:global(.byline-role-permissions-description) {
|
|
194
|
+
margin-bottom: 0;
|
|
195
|
+
font-size: var(--font-size-xs);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.mode-toggle,
|
|
199
|
+
:global(.byline-role-permissions-mode-toggle) {
|
|
200
|
+
display: inline-flex;
|
|
201
|
+
overflow: hidden;
|
|
202
|
+
border: var(--border-width-thin) var(--border-style-solid) var(--gray-200);
|
|
203
|
+
border-radius: var(--border-radius-sm);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.mode-button,
|
|
207
|
+
:global(.byline-role-permissions-mode-button) {
|
|
208
|
+
padding: 0.125rem 0.625rem;
|
|
209
|
+
font-size: var(--font-size-xs);
|
|
210
|
+
background-color: transparent;
|
|
211
|
+
border: 0;
|
|
212
|
+
cursor: pointer;
|
|
213
|
+
transition: background-color 150ms ease;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.mode-button:hover,
|
|
217
|
+
:global(.byline-role-permissions-mode-button):hover {
|
|
218
|
+
background-color: var(--gray-50);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.mode-button-active,
|
|
222
|
+
:global(.byline-role-permissions-mode-button-active) {
|
|
223
|
+
background-color: var(--gray-100);
|
|
224
|
+
font-weight: var(--font-weight-medium);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.mode-button-divider,
|
|
228
|
+
:global(.byline-role-permissions-mode-button-divider) {
|
|
229
|
+
border-left: var(--border-width-thin) var(--border-style-solid) var(--gray-200);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.mode-button-disabled,
|
|
233
|
+
:global(.byline-role-permissions-mode-button-disabled) {
|
|
234
|
+
opacity: 0.5;
|
|
235
|
+
cursor: not-allowed;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
:is([data-theme="dark"], :global(.dark)) {
|
|
239
|
+
.toolbar,
|
|
240
|
+
:global(.byline-role-permissions-toolbar) {
|
|
241
|
+
border-color: var(--gray-700);
|
|
242
|
+
background-color: var(--canvas-800);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.group,
|
|
246
|
+
:global(.byline-role-permissions-group) {
|
|
247
|
+
border-color: var(--gray-700);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.group-head,
|
|
251
|
+
:global(.byline-role-permissions-group-head) {
|
|
252
|
+
border-bottom-color: var(--gray-700);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.key,
|
|
256
|
+
:global(.byline-role-permissions-key) {
|
|
257
|
+
background-color: var(--canvas-800);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.mode-toggle,
|
|
261
|
+
:global(.byline-role-permissions-mode-toggle) {
|
|
262
|
+
border-color: var(--gray-700);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.mode-button:hover,
|
|
266
|
+
:global(.byline-role-permissions-mode-button):hover {
|
|
267
|
+
background-color: var(--canvas-700);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.mode-button-active,
|
|
271
|
+
:global(.byline-role-permissions-mode-button-active) {
|
|
272
|
+
background-color: var(--canvas-700);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.mode-button-divider,
|
|
276
|
+
:global(.byline-role-permissions-mode-button-divider) {
|
|
277
|
+
border-left-color: var(--gray-700);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
5
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
7
|
+
*
|
|
8
|
+
* Copyright (c) Infonomic Company Limited
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Inline per-role ability editor. Renders below the role-details
|
|
13
|
+
* header on the role detail page — full-width, no drawer.
|
|
14
|
+
*
|
|
15
|
+
* Mode-switched: defaults to **view** (checkboxes disabled, no edit
|
|
16
|
+
* affordances). Click Edit to switch to **edit** mode — checkboxes
|
|
17
|
+
* become interactive, per-group "Select all / Clear" buttons appear,
|
|
18
|
+
* and a Save button materialises once the selection diverges from the
|
|
19
|
+
* stored set. Cancel reverts the local set and returns to view mode.
|
|
20
|
+
*
|
|
21
|
+
* The View toggle is disabled while there are unsaved changes — this
|
|
22
|
+
* is the explicit-intent equivalent of a confirm dialog and keeps the
|
|
23
|
+
* UX honest about whether changes have been persisted.
|
|
24
|
+
*
|
|
25
|
+
* Vid-less by design (see Phase B notes): abilities live in
|
|
26
|
+
* `byline_admin_permissions`, not on the role row, so editing them
|
|
27
|
+
* does not bump the role's `vid`. Last-writer-wins on a per-role basis.
|
|
28
|
+
*
|
|
29
|
+
* Stable override handles: see `permissions.module.css`.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { useMemo, useState } from 'react'
|
|
33
|
+
|
|
34
|
+
import { Alert, Button, Checkbox, LoaderEllipsis } from '@byline/ui/react'
|
|
35
|
+
import cx from 'classnames'
|
|
36
|
+
|
|
37
|
+
import { useBylineAdminServices } from '../../../services/admin-services-context.js'
|
|
38
|
+
import styles from './permissions.module.css'
|
|
39
|
+
import type {
|
|
40
|
+
AbilityDescriptorResponse,
|
|
41
|
+
AbilityGroupResponse,
|
|
42
|
+
ListRegisteredAbilitiesResponse,
|
|
43
|
+
SetRoleAbilitiesResponse,
|
|
44
|
+
} from '../../admin-permissions/index.js'
|
|
45
|
+
import type { AdminRoleResponse } from '../index.js'
|
|
46
|
+
|
|
47
|
+
type Mode = 'view' | 'edit'
|
|
48
|
+
|
|
49
|
+
interface RolePermissionsProps {
|
|
50
|
+
role: AdminRoleResponse
|
|
51
|
+
registered: ListRegisteredAbilitiesResponse
|
|
52
|
+
initialAbilities: string[]
|
|
53
|
+
onSaved?: (response: SetRoleAbilitiesResponse) => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
|
|
57
|
+
if (a.size !== b.size) return false
|
|
58
|
+
for (const item of a) if (!b.has(item)) return false
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface GroupSectionProps {
|
|
63
|
+
group: AbilityGroupResponse
|
|
64
|
+
selected: ReadonlySet<string>
|
|
65
|
+
mode: Mode
|
|
66
|
+
saving: boolean
|
|
67
|
+
onToggle: (key: string, checked: boolean) => void
|
|
68
|
+
onSelectAll: (groupKeys: readonly string[]) => void
|
|
69
|
+
onClearAll: (groupKeys: readonly string[]) => void
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function GroupSection({
|
|
73
|
+
group,
|
|
74
|
+
selected,
|
|
75
|
+
mode,
|
|
76
|
+
saving,
|
|
77
|
+
onToggle,
|
|
78
|
+
onSelectAll,
|
|
79
|
+
onClearAll,
|
|
80
|
+
}: GroupSectionProps) {
|
|
81
|
+
const groupKeys = useMemo(() => group.abilities.map((a) => a.key), [group.abilities])
|
|
82
|
+
const selectedInGroup = groupKeys.filter((key) => selected.has(key)).length
|
|
83
|
+
const isEdit = mode === 'edit'
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className={cx('byline-role-permissions-group', styles.group)}>
|
|
87
|
+
<div className={cx('byline-role-permissions-group-head', styles['group-head'])}>
|
|
88
|
+
<div>
|
|
89
|
+
<span className={cx('byline-role-permissions-group-name', styles['group-name'])}>
|
|
90
|
+
{group.group}
|
|
91
|
+
</span>
|
|
92
|
+
<span
|
|
93
|
+
className={cx('muted', 'byline-role-permissions-group-count', styles['group-count'])}
|
|
94
|
+
>
|
|
95
|
+
{selectedInGroup} of {group.abilities.length} {isEdit ? 'selected' : 'granted'}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
{isEdit ? (
|
|
99
|
+
<div className={cx('byline-role-permissions-group-buttons', styles['group-buttons'])}>
|
|
100
|
+
<Button
|
|
101
|
+
size="xs"
|
|
102
|
+
intent="secondary"
|
|
103
|
+
type="button"
|
|
104
|
+
disabled={saving || selectedInGroup === group.abilities.length}
|
|
105
|
+
onClick={() => onSelectAll(groupKeys)}
|
|
106
|
+
>
|
|
107
|
+
Select all
|
|
108
|
+
</Button>
|
|
109
|
+
<Button
|
|
110
|
+
size="xs"
|
|
111
|
+
intent="secondary"
|
|
112
|
+
type="button"
|
|
113
|
+
disabled={saving || selectedInGroup === 0}
|
|
114
|
+
onClick={() => onClearAll(groupKeys)}
|
|
115
|
+
>
|
|
116
|
+
Clear
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
119
|
+
) : null}
|
|
120
|
+
</div>
|
|
121
|
+
<div className={cx('byline-role-permissions-grid', styles.grid)}>
|
|
122
|
+
{group.abilities.map((ability: AbilityDescriptorResponse) => (
|
|
123
|
+
<div key={ability.key} className={cx('byline-role-permissions-row', styles.row)}>
|
|
124
|
+
<Checkbox
|
|
125
|
+
id={`ability-${ability.key}`}
|
|
126
|
+
name={`ability-${ability.key}`}
|
|
127
|
+
checked={selected.has(ability.key)}
|
|
128
|
+
disabled={!isEdit || saving}
|
|
129
|
+
onCheckedChange={(checked) => onToggle(ability.key, checked === true)}
|
|
130
|
+
// Override the uikit Checkbox container's `width: 100%` so it
|
|
131
|
+
// shrinks to its button width — otherwise the external label
|
|
132
|
+
// is pushed away by an empty 100%-wide container.
|
|
133
|
+
containerClasses={cx(
|
|
134
|
+
'byline-role-permissions-checkbox-auto',
|
|
135
|
+
styles['checkbox-auto']
|
|
136
|
+
)}
|
|
137
|
+
componentClasses={cx(
|
|
138
|
+
'byline-role-permissions-checkbox-auto',
|
|
139
|
+
styles['checkbox-auto']
|
|
140
|
+
)}
|
|
141
|
+
/>
|
|
142
|
+
<label
|
|
143
|
+
htmlFor={`ability-${ability.key}`}
|
|
144
|
+
className={cx(
|
|
145
|
+
'byline-role-permissions-label',
|
|
146
|
+
styles.label,
|
|
147
|
+
isEdit && ['byline-role-permissions-label-edit', styles['label-edit']]
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
<div className={cx('byline-role-permissions-label-head', styles['label-head'])}>
|
|
151
|
+
<span className={cx('byline-role-permissions-label-name', styles['label-name'])}>
|
|
152
|
+
{ability.label}
|
|
153
|
+
</span>
|
|
154
|
+
<code className={cx('byline-role-permissions-key', styles.key)}>{ability.key}</code>
|
|
155
|
+
</div>
|
|
156
|
+
{ability.description ? (
|
|
157
|
+
<p
|
|
158
|
+
className={cx('muted', 'byline-role-permissions-description', styles.description)}
|
|
159
|
+
>
|
|
160
|
+
{ability.description}
|
|
161
|
+
</p>
|
|
162
|
+
) : null}
|
|
163
|
+
</label>
|
|
164
|
+
</div>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function RolePermissions({
|
|
172
|
+
role,
|
|
173
|
+
registered,
|
|
174
|
+
initialAbilities,
|
|
175
|
+
onSaved,
|
|
176
|
+
}: RolePermissionsProps) {
|
|
177
|
+
const { setRoleAbilities } = useBylineAdminServices()
|
|
178
|
+
const [mode, setMode] = useState<Mode>('view')
|
|
179
|
+
const [initialSet, setInitialSet] = useState<ReadonlySet<string>>(() => new Set(initialAbilities))
|
|
180
|
+
const [selected, setSelected] = useState<Set<string>>(() => new Set(initialAbilities))
|
|
181
|
+
const [saving, setSaving] = useState(false)
|
|
182
|
+
const [error, setError] = useState<string | null>(null)
|
|
183
|
+
|
|
184
|
+
const isDirty = !setsEqual(selected, initialSet)
|
|
185
|
+
const totalSelected = selected.size
|
|
186
|
+
|
|
187
|
+
function handleToggle(key: string, checked: boolean): void {
|
|
188
|
+
if (mode !== 'edit') return
|
|
189
|
+
setSelected((current) => {
|
|
190
|
+
const next = new Set(current)
|
|
191
|
+
if (checked) next.add(key)
|
|
192
|
+
else next.delete(key)
|
|
193
|
+
return next
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function handleSelectAll(groupKeys: readonly string[]): void {
|
|
198
|
+
setSelected((current) => {
|
|
199
|
+
const next = new Set(current)
|
|
200
|
+
for (const key of groupKeys) next.add(key)
|
|
201
|
+
return next
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function handleClearAll(groupKeys: readonly string[]): void {
|
|
206
|
+
setSelected((current) => {
|
|
207
|
+
const next = new Set(current)
|
|
208
|
+
for (const key of groupKeys) next.delete(key)
|
|
209
|
+
return next
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function handleCancel(): void {
|
|
214
|
+
setSelected(new Set(initialSet))
|
|
215
|
+
setError(null)
|
|
216
|
+
setMode('view')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function handleEnterEdit(): void {
|
|
220
|
+
setError(null)
|
|
221
|
+
setMode('edit')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function handleEnterView(): void {
|
|
225
|
+
// Disabled while dirty (the toggle's `disabled` prop guards this) —
|
|
226
|
+
// belt-and-suspenders so a stray click can't slip through.
|
|
227
|
+
if (isDirty) return
|
|
228
|
+
setError(null)
|
|
229
|
+
setMode('view')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function handleSave(): Promise<void> {
|
|
233
|
+
if (saving) return
|
|
234
|
+
setSaving(true)
|
|
235
|
+
setError(null)
|
|
236
|
+
try {
|
|
237
|
+
const response = await setRoleAbilities({
|
|
238
|
+
data: { id: role.id, abilities: Array.from(selected) },
|
|
239
|
+
})
|
|
240
|
+
// Reset baseline to the authoritative stored set — guards against
|
|
241
|
+
// any dedupe/normalisation the server might apply.
|
|
242
|
+
const storedSet = new Set(response.abilities)
|
|
243
|
+
setInitialSet(storedSet)
|
|
244
|
+
setSelected(new Set(storedSet))
|
|
245
|
+
onSaved?.(response)
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const code = getErrorCode(err)
|
|
248
|
+
if (code === 'admin.permissions.roleNotFound') {
|
|
249
|
+
setError('This role no longer exists.')
|
|
250
|
+
} else if (code === 'admin.permissions.abilityUnregistered') {
|
|
251
|
+
setError(
|
|
252
|
+
'One or more selected abilities are no longer registered. Reload the page and try again.'
|
|
253
|
+
)
|
|
254
|
+
} else {
|
|
255
|
+
setError('Could not save permissions. Please try again.')
|
|
256
|
+
}
|
|
257
|
+
} finally {
|
|
258
|
+
setSaving(false)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const isEdit = mode === 'edit'
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<div className={cx('byline-role-permissions-wrap', styles.wrap)}>
|
|
266
|
+
<div className={cx('byline-role-permissions-toolbar', styles.toolbar)}>
|
|
267
|
+
<ModeToggle
|
|
268
|
+
mode={mode}
|
|
269
|
+
dirty={isDirty}
|
|
270
|
+
saving={saving}
|
|
271
|
+
onView={handleEnterView}
|
|
272
|
+
onEdit={handleEnterEdit}
|
|
273
|
+
/>
|
|
274
|
+
<p className={cx('muted', 'byline-role-permissions-counter', styles.counter)}>
|
|
275
|
+
<span className={cx('byline-role-permissions-counter-num', styles['counter-num'])}>
|
|
276
|
+
{totalSelected}
|
|
277
|
+
</span>{' '}
|
|
278
|
+
of{' '}
|
|
279
|
+
<span className={cx('byline-role-permissions-counter-num', styles['counter-num'])}>
|
|
280
|
+
{registered.total}
|
|
281
|
+
</span>{' '}
|
|
282
|
+
{isEdit ? 'selected' : 'granted'} for {role.name}
|
|
283
|
+
</p>
|
|
284
|
+
{isEdit && isDirty ? (
|
|
285
|
+
<div className={cx('byline-role-permissions-actions', styles.actions)}>
|
|
286
|
+
<Button
|
|
287
|
+
type="button"
|
|
288
|
+
intent="secondary"
|
|
289
|
+
size="xs"
|
|
290
|
+
onClick={handleCancel}
|
|
291
|
+
disabled={saving}
|
|
292
|
+
className={cx('byline-role-permissions-action', styles.action)}
|
|
293
|
+
>
|
|
294
|
+
Cancel
|
|
295
|
+
</Button>
|
|
296
|
+
<Button
|
|
297
|
+
type="button"
|
|
298
|
+
intent="primary"
|
|
299
|
+
size="xs"
|
|
300
|
+
onClick={() => void handleSave()}
|
|
301
|
+
disabled={saving}
|
|
302
|
+
className={cx('byline-role-permissions-action', styles.action)}
|
|
303
|
+
>
|
|
304
|
+
{saving ? <LoaderEllipsis size={30} /> : 'Save'}
|
|
305
|
+
</Button>
|
|
306
|
+
</div>
|
|
307
|
+
) : null}
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{error ? <Alert intent="danger">{error}</Alert> : null}
|
|
311
|
+
|
|
312
|
+
<div className={cx('byline-role-permissions-groups', styles.groups)}>
|
|
313
|
+
{registered.groups.map((group) => (
|
|
314
|
+
<GroupSection
|
|
315
|
+
key={group.group}
|
|
316
|
+
group={group}
|
|
317
|
+
selected={selected}
|
|
318
|
+
mode={mode}
|
|
319
|
+
saving={saving}
|
|
320
|
+
onToggle={handleToggle}
|
|
321
|
+
onSelectAll={handleSelectAll}
|
|
322
|
+
onClearAll={handleClearAll}
|
|
323
|
+
/>
|
|
324
|
+
))}
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
interface ModeToggleProps {
|
|
331
|
+
mode: Mode
|
|
332
|
+
dirty: boolean
|
|
333
|
+
saving: boolean
|
|
334
|
+
onView: () => void
|
|
335
|
+
onEdit: () => void
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function ModeToggle({ mode, dirty, saving, onView, onEdit }: ModeToggleProps) {
|
|
339
|
+
// Segmented two-state toggle. View is disabled while dirty so the
|
|
340
|
+
// user has to commit to Save or Cancel — avoids accidentally
|
|
341
|
+
// discarding a draft selection.
|
|
342
|
+
const isView = mode === 'view'
|
|
343
|
+
const isEdit = mode === 'edit'
|
|
344
|
+
const viewDisabled = dirty || saving
|
|
345
|
+
const editDisabled = saving
|
|
346
|
+
return (
|
|
347
|
+
<div
|
|
348
|
+
role="group"
|
|
349
|
+
aria-label="Permissions mode"
|
|
350
|
+
className={cx('byline-role-permissions-mode-toggle', styles['mode-toggle'])}
|
|
351
|
+
>
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
onClick={onView}
|
|
355
|
+
disabled={viewDisabled}
|
|
356
|
+
className={cx(
|
|
357
|
+
'byline-role-permissions-mode-button',
|
|
358
|
+
styles['mode-button'],
|
|
359
|
+
isView && ['byline-role-permissions-mode-button-active', styles['mode-button-active']],
|
|
360
|
+
viewDisabled &&
|
|
361
|
+
!isView && [
|
|
362
|
+
'byline-role-permissions-mode-button-disabled',
|
|
363
|
+
styles['mode-button-disabled'],
|
|
364
|
+
]
|
|
365
|
+
)}
|
|
366
|
+
>
|
|
367
|
+
View
|
|
368
|
+
</button>
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
onClick={onEdit}
|
|
372
|
+
disabled={editDisabled}
|
|
373
|
+
className={cx(
|
|
374
|
+
'byline-role-permissions-mode-button',
|
|
375
|
+
'byline-role-permissions-mode-button-divider',
|
|
376
|
+
styles['mode-button'],
|
|
377
|
+
styles['mode-button-divider'],
|
|
378
|
+
isEdit && ['byline-role-permissions-mode-button-active', styles['mode-button-active']],
|
|
379
|
+
editDisabled &&
|
|
380
|
+
!isEdit && [
|
|
381
|
+
'byline-role-permissions-mode-button-disabled',
|
|
382
|
+
styles['mode-button-disabled'],
|
|
383
|
+
]
|
|
384
|
+
)}
|
|
385
|
+
>
|
|
386
|
+
Edit
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function getErrorCode(err: unknown): string | null {
|
|
393
|
+
return typeof (err as { code?: unknown })?.code === 'string'
|
|
394
|
+
? (err as { code: string }).code
|
|
395
|
+
: null
|
|
396
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UpdateAdminRole — drawer form for editing an existing role.
|
|
3
|
+
*
|
|
4
|
+
* Override handles:
|
|
5
|
+
* .byline-role-update-wrap — outer container
|
|
6
|
+
* .byline-role-update-form — vertical-stack form element
|
|
7
|
+
* .byline-role-update-actions — Cancel/Save row
|
|
8
|
+
* .byline-role-update-action — buttons in the actions row
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.wrap,
|
|
12
|
+
:global(.byline-role-update-wrap) {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
gap: var(--spacing-8);
|
|
16
|
+
padding: var(--spacing-4);
|
|
17
|
+
margin-top: var(--spacing-4);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.form,
|
|
21
|
+
:global(.byline-role-update-form) {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
gap: var(--spacing-16);
|
|
25
|
+
padding-top: var(--spacing-8);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.actions,
|
|
29
|
+
:global(.byline-role-update-actions) {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: flex-end;
|
|
33
|
+
gap: var(--spacing-8);
|
|
34
|
+
margin-top: var(--spacing-16);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.action,
|
|
38
|
+
:global(.byline-role-update-action) {
|
|
39
|
+
min-width: 4rem;
|
|
40
|
+
}
|