@plone-collective/volto-identity 1.0.0-alpha.13 → 1.0.0-alpha.14
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.draft +10 -2
- package/CHANGELOG.md +13 -0
- package/package.json +4 -4
- package/src/components/ControlPanel/ProviderForm.stories.tsx +2 -2
- package/src/components/Toolbar/Toolbar.jsx +1 -1
- package/src/components/UserMenu/UserAvatar.test.tsx +15 -0
- package/src/helpers/avatar.test.ts +3 -2
- package/src/helpers/groupmap.ts +5 -8
- package/src/helpers/providerSchema.test.ts +27 -9
- package/src/helpers/providerSchema.ts +20 -11
- package/src/stories/fixtures.tsx +10 -6
- package/src/styles.css +6 -0
package/.changelog.draft
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
## 1.0.0-alpha.
|
|
1
|
+
## 1.0.0-alpha.14 (2026-10-22)
|
|
2
2
|
|
|
3
|
-
No significant changes.
|
|
4
3
|
|
|
4
|
+
### Feature
|
|
5
|
+
|
|
6
|
+
- Made the local group in a provider's group map a picker over the site's groups, served by the backend. @ericof [#144](https://github.com/collective/pas-plugins-identity/issues/144)
|
|
7
|
+
- Drew the avatar on the toolbar's personal-tools button at 50px instead of 30px, and made the size a CSS variable, `--identity-toolbar-avatar-size`. A site that already sets `--toolbar-avatar-size` keeps its size, and no longer needs its own rule with `!important` to override the avatar's inline width and height. @ericof
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Internal
|
|
11
|
+
|
|
12
|
+
- Updated code comments that described the property map's target as four fixed fields. @ericof [#143](https://github.com/collective/pas-plugins-identity/issues/143)
|
|
5
13
|
|
|
6
14
|
|
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
<!-- towncrier release notes start -->
|
|
10
10
|
|
|
11
|
+
## 1.0.0-alpha.14 (2026-10-22)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Feature
|
|
15
|
+
|
|
16
|
+
- Made the local group in a provider's group map a picker over the site's groups, served by the backend. @ericof [#144](https://github.com/collective/pas-plugins-identity/issues/144)
|
|
17
|
+
- Drew the avatar on the toolbar's personal-tools button at 50px instead of 30px, and made the size a CSS variable, `--identity-toolbar-avatar-size`. A site that already sets `--toolbar-avatar-size` keeps its size, and no longer needs its own rule with `!important` to override the avatar's inline width and height. @ericof
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Internal
|
|
21
|
+
|
|
22
|
+
- Updated code comments that described the property map's target as four fixed fields. @ericof [#143](https://github.com/collective/pas-plugins-identity/issues/143)
|
|
23
|
+
|
|
11
24
|
## 1.0.0-alpha.13 (2026-09-30)
|
|
12
25
|
|
|
13
26
|
No significant changes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plone-collective/volto-identity",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.14",
|
|
4
4
|
"description": "Volto add-on for multi-provider external authentication in Plone, with identity linking",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
"react-aria-components": "^1.17.0",
|
|
51
51
|
"react-cookie": "8.1.2",
|
|
52
52
|
"@storybook/react": "^8.0.4",
|
|
53
|
-
"@plone/types": "2.0.1",
|
|
54
|
-
"@plone/components": "4.2.1",
|
|
55
53
|
"@plone/scripts": "4.0.1",
|
|
56
|
-
"@plone/registry": "3.0.1"
|
|
54
|
+
"@plone/registry": "3.0.1",
|
|
55
|
+
"@plone/components": "4.2.1",
|
|
56
|
+
"@plone/types": "2.0.1"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
@@ -22,7 +22,7 @@ import { providerSchema, toFormData } from '../../helpers/providerSchema';
|
|
|
22
22
|
*
|
|
23
23
|
* Both mappings are `object_list` widgets, and one sub-field of one of them
|
|
24
24
|
* reads a vocabulary over the API: the property map's target, which is a
|
|
25
|
-
* picker over the
|
|
25
|
+
* picker over the Profile fields a login writes. Every story carries the
|
|
26
26
|
* loaded vocabularies in its store, so that picker renders its terms rather
|
|
27
27
|
* than empty -- which is also exactly how it looks when the backend refuses
|
|
28
28
|
* `@vocabularies`, and is worth recognising.
|
|
@@ -59,7 +59,7 @@ const form = (provider: (typeof CONFIGURED)[number], driverId: string) => ({
|
|
|
59
59
|
* The **Attribute mapping** fieldset carries both editors, and the asymmetry
|
|
60
60
|
* between them is the thing to look at. A claim path and a provider-side
|
|
61
61
|
* group are text, because neither can be enumerated from here. A property
|
|
62
|
-
* map's target is a picker over the
|
|
62
|
+
* map's target is a picker over the Profile fields a login writes. A
|
|
63
63
|
* local group is text as well, though this site does know its own groups: a
|
|
64
64
|
* profile may ship a map before the group it points at, and the login skips a
|
|
65
65
|
* row it cannot resolve rather than the import refusing the map.
|
|
@@ -126,4 +126,19 @@ describe('UserAvatar', () => {
|
|
|
126
126
|
container.querySelector('.identity-avatar')?.getAttribute('style'),
|
|
127
127
|
).toContain('96px');
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
it('takes a CSS variable as its size, as the toolbar passes one', () => {
|
|
131
|
+
const { container } = renderAvatar(
|
|
132
|
+
{ id: 'alice' },
|
|
133
|
+
{ size: 'var(--identity-toolbar-avatar-size)' },
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const style = container
|
|
137
|
+
.querySelector('.identity-avatar')
|
|
138
|
+
?.getAttribute('style');
|
|
139
|
+
expect(style).toContain('width: var(--identity-toolbar-avatar-size)');
|
|
140
|
+
expect(style).toContain(
|
|
141
|
+
'font-size: calc(var(--identity-toolbar-avatar-size) / 2.4)',
|
|
142
|
+
);
|
|
143
|
+
});
|
|
129
144
|
});
|
|
@@ -148,8 +148,9 @@ describe('avatarColors', () => {
|
|
|
148
148
|
it.each([...DEFAULT_AVATAR_COLORS])(
|
|
149
149
|
'ships %s, which clears WCAG AA against the white initials',
|
|
150
150
|
(hex) => {
|
|
151
|
-
// 4.5:1, the ratio for normal text
|
|
152
|
-
// size
|
|
151
|
+
// 4.5:1, the ratio for normal text, so the palette holds whatever
|
|
152
|
+
// `--identity-toolbar-avatar-size` a site sets: at 30px the initials
|
|
153
|
+
// are well under the size WCAG counts as large.
|
|
153
154
|
const ratio = (luminance('#ffffff') + 0.05) / (luminance(hex) + 0.05);
|
|
154
155
|
|
|
155
156
|
expect(ratio).toBeGreaterThanOrEqual(4.5);
|
package/src/helpers/groupmap.ts
CHANGED
|
@@ -5,14 +5,11 @@
|
|
|
5
5
|
* {@link module:helpers/rowmap}; what belongs here is only the names this
|
|
6
6
|
* mapping's two halves go by.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* points at, so the field stays text rather than a `Choice` that would refuse
|
|
14
|
-
* the import. Compare the property map, whose target is four fields this
|
|
15
|
-
* package knows by name and is a picker.
|
|
8
|
+
* The provider side is free text: it is whatever the far end's directory
|
|
9
|
+
* happens to call a group, which this site cannot enumerate. The local side
|
|
10
|
+
* is a pick from this site's groups, served by the backend, and the API
|
|
11
|
+
* refuses a group that does not exist. Only a profile's import may name one,
|
|
12
|
+
* because it may ship a map before the group it points at.
|
|
16
13
|
* @module helpers/groupmap
|
|
17
14
|
*/
|
|
18
15
|
|
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
suggestedProviderId,
|
|
21
21
|
toFormData,
|
|
22
22
|
} from './providerSchema';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
GROUPS_VOCABULARY,
|
|
25
|
+
USER_FIELDS_VOCABULARY,
|
|
26
|
+
} from '../constants/vocabularies';
|
|
24
27
|
import type { Driver, VoltoSchema } from '../types';
|
|
25
28
|
|
|
26
29
|
const intl = {
|
|
@@ -28,7 +31,7 @@ const intl = {
|
|
|
28
31
|
} as any;
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
|
-
* What `@identity-providers` sends: `
|
|
34
|
+
* What `@identity-providers` sends: `IProviderForm`, serialized.
|
|
32
35
|
*
|
|
33
36
|
* Including `driver`, `propertymap` and `groupmap`, which it really does
|
|
34
37
|
* send -- they are fields of a provider's storage. Leaving them out of this
|
|
@@ -61,9 +64,13 @@ const SERVED = {
|
|
|
61
64
|
schema: { title: 'Provider group', type: 'string' },
|
|
62
65
|
additional: {},
|
|
63
66
|
},
|
|
67
|
+
// The local side is a `Choice` over the site's groups on the served
|
|
68
|
+
// schema, though not on the records it is stored in.
|
|
64
69
|
value_type: {
|
|
65
|
-
schema: { title: 'Local group', type: 'string' },
|
|
66
|
-
additional: {
|
|
70
|
+
schema: { title: 'Local group', type: 'string', factory: 'Choice' },
|
|
71
|
+
additional: {
|
|
72
|
+
vocabulary: { '@id': `/@vocabularies/${GROUPS_VOCABULARY}` },
|
|
73
|
+
},
|
|
67
74
|
},
|
|
68
75
|
},
|
|
69
76
|
icon: { title: 'Icon', type: 'string', widget: 'provider_icon' },
|
|
@@ -280,15 +287,23 @@ describe('providerSchema', () => {
|
|
|
280
287
|
expect(property.properties.claim).not.toHaveProperty('vocabulary');
|
|
281
288
|
});
|
|
282
289
|
|
|
283
|
-
it('
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
290
|
+
it("makes the group map's local side a picker over the served groups", () => {
|
|
291
|
+
// A typed id with a typo, or a title where the id belongs, saved without
|
|
292
|
+
// complaint and granted nothing at every login.
|
|
293
|
+
const schema = providerSchema(SERVED, DRIVERS, 'oidc', false, intl);
|
|
294
|
+
const group = schema.properties.groupmap?.schema as VoltoSchema;
|
|
295
|
+
|
|
296
|
+
expect((group.properties.local as any).vocabulary['@id']).toBe(
|
|
297
|
+
`/@vocabularies/${GROUPS_VOCABULARY}`,
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("leaves the group map's provider side a text box", () => {
|
|
302
|
+
// The far end's own group names, which nothing here enumerates.
|
|
287
303
|
const schema = providerSchema(SERVED, DRIVERS, 'oidc', false, intl);
|
|
288
304
|
const group = schema.properties.groupmap?.schema as VoltoSchema;
|
|
289
305
|
|
|
290
306
|
expect(group.properties.group).not.toHaveProperty('vocabulary');
|
|
291
|
-
expect(group.properties.local).not.toHaveProperty('vocabulary');
|
|
292
307
|
});
|
|
293
308
|
|
|
294
309
|
it('falls back to a text box when no vocabulary is served', () => {
|
|
@@ -299,14 +314,17 @@ describe('providerSchema', () => {
|
|
|
299
314
|
properties: {
|
|
300
315
|
...SERVED.properties,
|
|
301
316
|
propertymap: { title: 'Property map', type: 'object' },
|
|
317
|
+
groupmap: { title: 'Group map', type: 'object' },
|
|
302
318
|
},
|
|
303
319
|
};
|
|
304
320
|
|
|
305
321
|
const schema = providerSchema(older, DRIVERS, 'oidc', false, intl);
|
|
306
322
|
const property = schema.properties.propertymap?.schema as VoltoSchema;
|
|
323
|
+
const group = schema.properties.groupmap?.schema as VoltoSchema;
|
|
307
324
|
|
|
308
325
|
expect(property.properties.field).not.toHaveProperty('vocabulary');
|
|
309
326
|
expect(property.properties.field.type).toBe('string');
|
|
327
|
+
expect(group.properties.local).not.toHaveProperty('vocabulary');
|
|
310
328
|
});
|
|
311
329
|
|
|
312
330
|
it('drops the served driver field rather than merging it', () => {
|
|
@@ -265,9 +265,9 @@ export function providerSchema(
|
|
|
265
265
|
// A picker over the fields a login writes, not a text box. The two
|
|
266
266
|
// sides of this row are asymmetric for the same reason the group
|
|
267
267
|
// map's are: a claim path is whatever the far end publishes and
|
|
268
|
-
// nothing here can enumerate it, while the target is
|
|
269
|
-
//
|
|
270
|
-
vocabulary: targetVocabulary(served),
|
|
268
|
+
// nothing here can enumerate it, while the target is a field of
|
|
269
|
+
// the site's user type, which the backend serves.
|
|
270
|
+
vocabulary: targetVocabulary(served, 'propertymap'),
|
|
271
271
|
},
|
|
272
272
|
),
|
|
273
273
|
};
|
|
@@ -285,7 +285,13 @@ export function providerSchema(
|
|
|
285
285
|
schema: rowSchema(
|
|
286
286
|
intl.formatMessage(messages.groupmapRow),
|
|
287
287
|
{ name: 'group', title: intl.formatMessage(messages.providerGroup) },
|
|
288
|
-
{
|
|
288
|
+
{
|
|
289
|
+
name: 'local',
|
|
290
|
+
title: intl.formatMessage(messages.localGroup),
|
|
291
|
+
// A pick from this site's groups, for the reason the property
|
|
292
|
+
// map's target is one: a typed id with a typo granted nothing.
|
|
293
|
+
vocabulary: targetVocabulary(served, 'groupmap'),
|
|
294
|
+
},
|
|
289
295
|
),
|
|
290
296
|
};
|
|
291
297
|
}
|
|
@@ -351,21 +357,24 @@ interface VocabularyRef {
|
|
|
351
357
|
}
|
|
352
358
|
|
|
353
359
|
/**
|
|
354
|
-
* Read the vocabulary the backend serves for
|
|
360
|
+
* Read the vocabulary the backend serves for one mapping's values.
|
|
355
361
|
*
|
|
356
|
-
*
|
|
357
|
-
* `pas.plugins.identity.UserFields`,
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
362
|
+
* Both mappings are a `Dict` whose values are a `Choice` on the served
|
|
363
|
+
* schema: the property map's over `pas.plugins.identity.UserFields`, the
|
|
364
|
+
* group map's over `pas.plugins.identity.Groups`. `plone.restapi`
|
|
365
|
+
* serializes a named vocabulary as the URL that enumerates it. Lifted from
|
|
366
|
+
* the served schema rather than assembled here: the name of the vocabulary,
|
|
367
|
+
* and whether there is one at all, are the backend's to say.
|
|
361
368
|
*
|
|
362
369
|
* @param served The provider schema from `@identity-providers`.
|
|
370
|
+
* @param mapping Which mapping, `propertymap` or `groupmap`.
|
|
363
371
|
* @returns The vocabulary reference, or undefined when none is served.
|
|
364
372
|
*/
|
|
365
373
|
function targetVocabulary(
|
|
366
374
|
served: JsonSchema | undefined,
|
|
375
|
+
mapping: 'propertymap' | 'groupmap',
|
|
367
376
|
): VocabularyRef | undefined {
|
|
368
|
-
const value = (served?.properties?.
|
|
377
|
+
const value = (served?.properties?.[mapping] as any)?.value_type;
|
|
369
378
|
const reference = value?.additional?.vocabulary;
|
|
370
379
|
return reference?.['@id'] ? reference : undefined;
|
|
371
380
|
}
|
package/src/stories/fixtures.tsx
CHANGED
|
@@ -418,8 +418,12 @@ export const PROVIDER_SCHEMA = {
|
|
|
418
418
|
additional: {},
|
|
419
419
|
},
|
|
420
420
|
value_type: {
|
|
421
|
-
schema: { type: 'string', title: 'Local group' },
|
|
422
|
-
additional: {
|
|
421
|
+
schema: { type: 'string', title: 'Local group', factory: 'Choice' },
|
|
422
|
+
additional: {
|
|
423
|
+
vocabulary: {
|
|
424
|
+
'@id': `http://localhost:8080/Plone/@vocabularies/${GROUPS_VOCABULARY}`,
|
|
425
|
+
},
|
|
426
|
+
},
|
|
423
427
|
},
|
|
424
428
|
},
|
|
425
429
|
title: { type: 'string', title: 'Title' },
|
|
@@ -498,10 +502,10 @@ export const CONFIGURED: ConfiguredProvider[] = [
|
|
|
498
502
|
/**
|
|
499
503
|
* The vocabulary the property-map editor reads, already loaded.
|
|
500
504
|
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
+
* The four terms of the shipped user type. A site whose type has behaviors
|
|
506
|
+
* gets their fields too. It used to be the site's live member schema, which
|
|
507
|
+
* also offered `email`, `portrait` and a username -- rows that were stored
|
|
508
|
+
* and then dropped on every login.
|
|
505
509
|
*/
|
|
506
510
|
export const USER_FIELDS_STATE = {
|
|
507
511
|
[USER_FIELDS_VOCABULARY]: {
|
package/src/styles.css
CHANGED
|
@@ -89,6 +89,12 @@
|
|
|
89
89
|
--identity-provider-oidc-bg-hover: #555;
|
|
90
90
|
--identity-provider-oidc-color: #fff;
|
|
91
91
|
|
|
92
|
+
/* The avatar on the toolbar's personal-tools button, picture and initials
|
|
93
|
+
alike. Passed to `<UserAvatar>` as its size, so it is the inline width
|
|
94
|
+
and height and needs no `!important` to win. `--toolbar-avatar-size` is
|
|
95
|
+
the name sites already set to enlarge it, and it still does. */
|
|
96
|
+
--identity-toolbar-avatar-size: var(--toolbar-avatar-size, 50px);
|
|
97
|
+
|
|
92
98
|
/* Stats boxes: a figure over what it counts, as on a group page's
|
|
93
99
|
`42 members · 12 directly`. A box is at least this wide and grows for a
|
|
94
100
|
long label rather than breaking it -- a translated label is often longer
|