@idp.global/catalog 3.0.0 → 3.1.0
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 +11 -0
- package/dist_bundle/bundle.js +835 -628
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/idp-admin-shell.d.ts +17 -0
- package/dist_ts_web/elements/idp-admin-shell.js +207 -27
- package/dist_ts_web/elements/idp-admin-sidebar.js +5 -5
- package/dist_ts_web/elements/idp-invite-card.d.ts +39 -0
- package/dist_ts_web/elements/idp-invite-card.js +303 -0
- package/dist_ts_web/elements/index.d.ts +1 -0
- package/dist_ts_web/elements/index.js +2 -1
- package/package.json +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/idp-admin-shell.ts +200 -25
- package/ts_web/elements/idp-admin-sidebar.ts +4 -4
- package/ts_web/elements/idp-invite-card.ts +229 -0
- package/ts_web/elements/index.ts +1 -0
|
@@ -94,6 +94,12 @@ export interface IIdpAdminGaUserActionEventDetail {
|
|
|
94
94
|
action: 'suspend' | 'unsuspend';
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export interface IIdpAdminProfileUpdateEventDetail {
|
|
98
|
+
name: string;
|
|
99
|
+
username: string;
|
|
100
|
+
mobileNumber: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
97
103
|
export interface IIdpAdminGaOrgActionEventDetail {
|
|
98
104
|
orgId: string;
|
|
99
105
|
action: 'suspend' | 'unsuspend';
|
|
@@ -831,13 +837,88 @@ export class IdpAdminShell extends DeesElement {
|
|
|
831
837
|
.mapping-row .form-label {
|
|
832
838
|
margin-bottom: 4px;
|
|
833
839
|
}
|
|
840
|
+
.mobile-topbar {
|
|
841
|
+
display: none;
|
|
842
|
+
align-items: center;
|
|
843
|
+
gap: 10px;
|
|
844
|
+
padding: 10px 14px;
|
|
845
|
+
border-bottom: 1px solid var(--sh-border);
|
|
846
|
+
background: var(--sh-card);
|
|
847
|
+
position: sticky;
|
|
848
|
+
top: 0;
|
|
849
|
+
z-index: 40;
|
|
850
|
+
}
|
|
851
|
+
.mobile-topbar-title {
|
|
852
|
+
font-size: 13px;
|
|
853
|
+
font-weight: 650;
|
|
854
|
+
letter-spacing: -0.01em;
|
|
855
|
+
}
|
|
856
|
+
.mobile-nav-toggle,
|
|
857
|
+
.mobile-drawer-close {
|
|
858
|
+
display: inline-flex;
|
|
859
|
+
align-items: center;
|
|
860
|
+
justify-content: center;
|
|
861
|
+
width: 34px;
|
|
862
|
+
height: 34px;
|
|
863
|
+
border: 1px solid var(--sh-border);
|
|
864
|
+
border-radius: 9px;
|
|
865
|
+
background: var(--sh-card);
|
|
866
|
+
color: var(--sh-fg);
|
|
867
|
+
cursor: pointer;
|
|
868
|
+
}
|
|
869
|
+
.mobile-nav-toggle:focus-visible,
|
|
870
|
+
.mobile-drawer-close:focus-visible {
|
|
871
|
+
outline: 2px solid var(--sh-accent);
|
|
872
|
+
outline-offset: 2px;
|
|
873
|
+
}
|
|
874
|
+
.mobile-drawer-backdrop {
|
|
875
|
+
position: fixed;
|
|
876
|
+
inset: 0;
|
|
877
|
+
background: rgba(0, 0, 0, 0.42);
|
|
878
|
+
z-index: 60;
|
|
879
|
+
}
|
|
880
|
+
.mobile-drawer {
|
|
881
|
+
position: fixed;
|
|
882
|
+
top: 0;
|
|
883
|
+
left: 0;
|
|
884
|
+
bottom: 0;
|
|
885
|
+
width: min(300px, 86vw);
|
|
886
|
+
background: var(--sh-card);
|
|
887
|
+
border-right: 1px solid var(--sh-border);
|
|
888
|
+
z-index: 61;
|
|
889
|
+
overflow-y: auto;
|
|
890
|
+
animation: drawer-in 0.18s ease-out;
|
|
891
|
+
}
|
|
892
|
+
.mobile-drawer idp-admin-sidebar {
|
|
893
|
+
display: block;
|
|
894
|
+
}
|
|
895
|
+
.mobile-drawer-close {
|
|
896
|
+
position: absolute;
|
|
897
|
+
top: 10px;
|
|
898
|
+
right: 10px;
|
|
899
|
+
z-index: 62;
|
|
900
|
+
}
|
|
901
|
+
@keyframes drawer-in {
|
|
902
|
+
from {
|
|
903
|
+
transform: translateX(-24px);
|
|
904
|
+
opacity: 0;
|
|
905
|
+
}
|
|
906
|
+
to {
|
|
907
|
+
transform: translateX(0);
|
|
908
|
+
opacity: 1;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
834
911
|
@media (max-width: 1120px) {
|
|
835
912
|
.shell {
|
|
836
913
|
grid-template-columns: 1fr;
|
|
914
|
+
grid-template-rows: auto 1fr;
|
|
837
915
|
}
|
|
838
|
-
idp-admin-sidebar {
|
|
916
|
+
.shell > idp-admin-sidebar {
|
|
839
917
|
display: none;
|
|
840
918
|
}
|
|
919
|
+
.mobile-topbar {
|
|
920
|
+
display: flex;
|
|
921
|
+
}
|
|
841
922
|
}
|
|
842
923
|
@media (max-width: 720px) {
|
|
843
924
|
.mapping-row {
|
|
@@ -943,6 +1024,20 @@ export class IdpAdminShell extends DeesElement {
|
|
|
943
1024
|
@property({ type: Array })
|
|
944
1025
|
public accessor delegations: IIdpDelegation[] = [];
|
|
945
1026
|
|
|
1027
|
+
/** enables editing of name/username/mobile on the profile page */
|
|
1028
|
+
@property({ type: Boolean })
|
|
1029
|
+
public accessor profileEditable = false;
|
|
1030
|
+
|
|
1031
|
+
/** host sets this while a profile update request is in flight */
|
|
1032
|
+
@property({ type: Boolean })
|
|
1033
|
+
public accessor profileSaving = false;
|
|
1034
|
+
|
|
1035
|
+
@state()
|
|
1036
|
+
private accessor profileDraft: { name: string; username: string; mobileNumber: string } | null = null;
|
|
1037
|
+
|
|
1038
|
+
@state()
|
|
1039
|
+
private accessor mobileNavOpen = false;
|
|
1040
|
+
|
|
946
1041
|
@state()
|
|
947
1042
|
private accessor logoutEverywhere = false;
|
|
948
1043
|
|
|
@@ -1336,6 +1431,22 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1336
1431
|
await super.disconnectedCallback();
|
|
1337
1432
|
}
|
|
1338
1433
|
|
|
1434
|
+
protected override willUpdate(changedProperties: Map<PropertyKey, unknown>) {
|
|
1435
|
+
super.willUpdate(changedProperties);
|
|
1436
|
+
// a refreshed user means the host persisted (or reloaded) the profile
|
|
1437
|
+
if (changedProperties.has('user')) {
|
|
1438
|
+
this.profileDraft = null;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
protected override updated(changedProperties: Map<PropertyKey, unknown>) {
|
|
1443
|
+
super.updated(changedProperties);
|
|
1444
|
+
if (changedProperties.has('mobileNavOpen') && this.mobileNavOpen) {
|
|
1445
|
+
const closeButton = this.shadowRoot?.querySelector('.mobile-drawer-close') as HTMLElement | null;
|
|
1446
|
+
closeButton?.focus();
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1339
1450
|
private dispatchShellEvent<TDetail>(eventNameArg: string, detailArg: TDetail) {
|
|
1340
1451
|
this.dispatchEvent(new CustomEvent<TDetail>(eventNameArg, {
|
|
1341
1452
|
detail: detailArg,
|
|
@@ -1778,18 +1889,54 @@ export class IdpAdminShell extends DeesElement {
|
|
|
1778
1889
|
`;
|
|
1779
1890
|
}
|
|
1780
1891
|
|
|
1892
|
+
private getProfileDraft() {
|
|
1893
|
+
return (
|
|
1894
|
+
this.profileDraft || {
|
|
1895
|
+
name: this.user?.name || '',
|
|
1896
|
+
username: this.user?.username || this.user?.email?.split('@')[0] || '',
|
|
1897
|
+
mobileNumber: this.user?.mobileNumber || '',
|
|
1898
|
+
}
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
private setProfileDraftField(fieldArg: 'name' | 'username' | 'mobileNumber', eventArg: Event) {
|
|
1903
|
+
const value = (eventArg.target as HTMLInputElement).value;
|
|
1904
|
+
this.profileDraft = { ...this.getProfileDraft(), [fieldArg]: value };
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
private handleProfileSave() {
|
|
1908
|
+
const draft = this.getProfileDraft();
|
|
1909
|
+
this.dispatchShellEvent<IIdpAdminProfileUpdateEventDetail>('idp-admin-profile-update', {
|
|
1910
|
+
name: draft.name.trim(),
|
|
1911
|
+
username: draft.username.trim(),
|
|
1912
|
+
mobileNumber: draft.mobileNumber.trim(),
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1781
1916
|
private renderProfile(): TemplateResult {
|
|
1782
|
-
const username = this.user?.username || this.user?.email?.split('@')[0] || '';
|
|
1783
1917
|
const status = this.user?.status || 'active';
|
|
1918
|
+
const draft = this.getProfileDraft();
|
|
1919
|
+
const editable = this.profileEditable;
|
|
1920
|
+
const dirty =
|
|
1921
|
+
this.profileDraft !== null &&
|
|
1922
|
+
(draft.name !== (this.user?.name || '') ||
|
|
1923
|
+
draft.username !== (this.user?.username || this.user?.email?.split('@')[0] || '') ||
|
|
1924
|
+
draft.mobileNumber !== (this.user?.mobileNumber || ''));
|
|
1784
1925
|
return html`
|
|
1785
1926
|
${this.renderPageHeader('Profile', 'Your personal identity details visible to connected apps.')}
|
|
1786
1927
|
<div class="body narrow-body">
|
|
1787
|
-
${this.renderSectionCard('Avatar', 'Shown to apps that request your profile.', html`<div class="split-row" style="justify-content:flex-start"><span class="avatar" style="width:56px;height:56px;font-size:20px">${this.userInitials}</span><div><div class="section-title">${this.user?.name || 'Unknown User'}</div><div class="muted" style="margin-top:5px"
|
|
1928
|
+
${this.renderSectionCard('Avatar', 'Shown to apps that request your profile.', html`<div class="split-row" style="justify-content:flex-start"><span class="avatar" style="width:56px;height:56px;font-size:20px">${this.userInitials}</span><div><div class="section-title">${this.user?.name || 'Unknown User'}</div><div class="muted" style="margin-top:5px">${this.user?.email || ''}</div></div></div>`)}
|
|
1788
1929
|
${this.renderSectionCard('Personal information', '', html`
|
|
1789
|
-
${this.renderFormRow('Full name', '', html`<input class="input" .value=${
|
|
1790
|
-
${this.renderFormRow('Username', 'Used in your public profile URL', html`<div class="input-group"><span class="input-prefix">idp.global/user/</span><input class="input" .value=${username} disabled /></div>`)}
|
|
1791
|
-
${this.renderFormRow('Email', 'Primary address for login and notifications', html`<input class="input" .value=${this.user?.email || ''} disabled />`)}
|
|
1792
|
-
${this.renderFormRow('Mobile number', '
|
|
1930
|
+
${this.renderFormRow('Full name', '', html`<input class="input" .value=${draft.name} ?disabled=${!editable || this.profileSaving} @input=${(eventArg: Event) => this.setProfileDraftField('name', eventArg)} />`, true)}
|
|
1931
|
+
${this.renderFormRow('Username', 'Used in your public profile URL', html`<div class="input-group"><span class="input-prefix">idp.global/user/</span><input class="input" .value=${draft.username} ?disabled=${!editable || this.profileSaving} @input=${(eventArg: Event) => this.setProfileDraftField('username', eventArg)} /></div>`)}
|
|
1932
|
+
${this.renderFormRow('Email', 'Primary address for login and notifications - contact support to change it', html`<input class="input" .value=${this.user?.email || ''} disabled />`)}
|
|
1933
|
+
${this.renderFormRow('Mobile number', 'Optional contact number shown to connected apps', html`<input class="input" .value=${draft.mobileNumber} ?disabled=${!editable || this.profileSaving} @input=${(eventArg: Event) => this.setProfileDraftField('mobileNumber', eventArg)} />`)}
|
|
1934
|
+
${editable
|
|
1935
|
+
? html`<div class="split-row" style="justify-content:flex-end;gap:10px;margin-top:14px">
|
|
1936
|
+
<idp-button variant="outline" shape="rect" ?disabled=${!dirty || this.profileSaving} @click=${() => { this.profileDraft = null; }}>Reset</idp-button>
|
|
1937
|
+
<idp-button variant="accent" shape="rect" ?loading=${this.profileSaving} ?disabled=${!dirty} @click=${this.handleProfileSave}>Save changes</idp-button>
|
|
1938
|
+
</div>`
|
|
1939
|
+
: html``}
|
|
1793
1940
|
`)}
|
|
1794
1941
|
${this.renderSectionCard('Account status', '', html`<div class="split-row"><div><div class="section-title">Status</div><div class="muted">Your account is currently ${status}.</div></div><idp-badge variant=${status === 'active' ? 'ok' : status === 'suspended' ? 'error' : 'warn'}>${status}</idp-badge></div>${this.globalAdmin ? html`<div class="divider"></div><div class="split-row"><div><div class="section-title">Global admin</div><div class="muted">You have platform-wide administrative access.</div></div><idp-badge variant="accent">Admin</idp-badge></div>` : html``}`)}
|
|
1795
1942
|
${this.delegationApps.length
|
|
@@ -2329,6 +2476,7 @@ export class IdpAdminShell extends DeesElement {
|
|
|
2329
2476
|
const planVariant = (planArg: string): 'accent' | 'ok' | 'warn' | 'default' =>
|
|
2330
2477
|
planArg === 'Pro' ? 'accent' : planArg === 'Enterprise' ? 'ok' : planArg === 'Internal' ? 'warn' : 'default';
|
|
2331
2478
|
const hasAdminFields = this.orgs.some((orgArg) => orgArg.status || orgArg.plan);
|
|
2479
|
+
const hasMemberCounts = this.orgs.some((orgArg) => orgArg.memberCount !== undefined);
|
|
2332
2480
|
const orgRows = this.orgs.map((orgArg) => ({
|
|
2333
2481
|
cells: [
|
|
2334
2482
|
html`
|
|
@@ -2341,9 +2489,9 @@ export class IdpAdminShell extends DeesElement {
|
|
|
2341
2489
|
</div>
|
|
2342
2490
|
`,
|
|
2343
2491
|
orgArg.slug || '-',
|
|
2492
|
+
...(hasMemberCounts ? [orgArg.memberCount !== undefined ? String(orgArg.memberCount) : '-'] : []),
|
|
2344
2493
|
...(hasAdminFields
|
|
2345
2494
|
? [
|
|
2346
|
-
orgArg.memberCount !== undefined ? String(orgArg.memberCount) : '-',
|
|
2347
2495
|
orgArg.plan ? html`<idp-badge variant=${planVariant(orgArg.plan)}>${orgArg.plan}</idp-badge>` : '-',
|
|
2348
2496
|
html`<idp-badge variant=${orgArg.status === 'suspended' ? 'error' : 'ok'}>${orgArg.status || 'active'}</idp-badge>`,
|
|
2349
2497
|
html`<button class="plain-button ghost" style="font-size:11px" @click=${() => this.dispatchShellEvent<IIdpAdminGaOrgActionEventDetail>('idp-admin-ga-org-action', { orgId: orgArg.id, action: orgArg.status === 'suspended' ? 'unsuspend' : 'suspend' })}>${orgArg.status === 'suspended' ? 'Unsuspend' : 'Suspend'}</button>`,
|
|
@@ -2363,21 +2511,17 @@ export class IdpAdminShell extends DeesElement {
|
|
|
2363
2511
|
badge=${`${this.orgs.length} total`}
|
|
2364
2512
|
empty-title="No organisations"
|
|
2365
2513
|
empty-description="No organisations are visible to this admin session."
|
|
2366
|
-
.columns=${
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
{ label: 'Slug', mono: true, hideBelow: 'mobile' },
|
|
2378
|
-
{ label: 'Role' },
|
|
2379
|
-
{ label: 'Identifier', mono: true, hideBelow: 'tablet' },
|
|
2380
|
-
]}
|
|
2514
|
+
.columns=${[
|
|
2515
|
+
{ label: 'Organisation' },
|
|
2516
|
+
{ label: 'Slug', mono: true, hideBelow: 'mobile' },
|
|
2517
|
+
...(hasMemberCounts ? [{ label: 'Members', mono: true, hideBelow: 'mobile' }] : []),
|
|
2518
|
+
...(hasAdminFields
|
|
2519
|
+
? [{ label: 'Plan' }, { label: 'Status' }, { label: '' }]
|
|
2520
|
+
: [
|
|
2521
|
+
{ label: 'Role' },
|
|
2522
|
+
{ label: 'Identifier', mono: true, hideBelow: 'tablet' },
|
|
2523
|
+
]),
|
|
2524
|
+
]}
|
|
2381
2525
|
.rows=${orgRows}
|
|
2382
2526
|
></idp-data-table>
|
|
2383
2527
|
</div>
|
|
@@ -2454,15 +2598,27 @@ export class IdpAdminShell extends DeesElement {
|
|
|
2454
2598
|
return (renderers[this.page] || renderers.overview)();
|
|
2455
2599
|
}
|
|
2456
2600
|
|
|
2601
|
+
private handleMobileNavKeydown(eventArg: KeyboardEvent) {
|
|
2602
|
+
if (eventArg.key === 'Escape') {
|
|
2603
|
+
this.mobileNavOpen = false;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2457
2607
|
public render(): TemplateResult {
|
|
2458
2608
|
return html`
|
|
2459
2609
|
<div
|
|
2460
2610
|
class="shell"
|
|
2461
|
-
@idp-admin-navigate=${this.handleSidebarNavigate}
|
|
2611
|
+
@idp-admin-navigate=${(eventArg: CustomEvent) => { this.mobileNavOpen = false; this.handleSidebarNavigate(eventArg as CustomEvent<IIdpAdminNavigateEventDetail>); }}
|
|
2462
2612
|
@idp-admin-org-select=${this.handleSidebarOrgSelect}
|
|
2463
2613
|
@idp-admin-org-create-request=${this.requestOrgCreate}
|
|
2464
2614
|
@idp-admin-logout-request=${this.requestLogout}
|
|
2465
2615
|
>
|
|
2616
|
+
<div class="mobile-topbar">
|
|
2617
|
+
<button class="mobile-nav-toggle" aria-label="Open navigation" aria-haspopup="dialog" aria-expanded=${this.mobileNavOpen} @click=${() => { this.mobileNavOpen = true; }}>
|
|
2618
|
+
<idp-icon name="lucide:Menu" size="17"></idp-icon>
|
|
2619
|
+
</button>
|
|
2620
|
+
<span class="mobile-topbar-title">idp.global</span>
|
|
2621
|
+
</div>
|
|
2466
2622
|
<idp-admin-sidebar
|
|
2467
2623
|
.page=${this.page}
|
|
2468
2624
|
.user=${this.user}
|
|
@@ -2472,12 +2628,31 @@ export class IdpAdminShell extends DeesElement {
|
|
|
2472
2628
|
.globalAdmin=${this.globalAdmin}
|
|
2473
2629
|
.sessionCount=${this.sessions.length}
|
|
2474
2630
|
></idp-admin-sidebar>
|
|
2631
|
+
${this.mobileNavOpen
|
|
2632
|
+
? html`
|
|
2633
|
+
<div class="mobile-drawer-backdrop" @click=${() => { this.mobileNavOpen = false; }}></div>
|
|
2634
|
+
<div class="mobile-drawer" role="dialog" aria-label="Navigation" @keydown=${this.handleMobileNavKeydown}>
|
|
2635
|
+
<button class="mobile-drawer-close" aria-label="Close navigation" @click=${() => { this.mobileNavOpen = false; }}>
|
|
2636
|
+
<idp-icon name="x" size="16"></idp-icon>
|
|
2637
|
+
</button>
|
|
2638
|
+
<idp-admin-sidebar
|
|
2639
|
+
.page=${this.page}
|
|
2640
|
+
.user=${this.user}
|
|
2641
|
+
.accounts=${this.accounts}
|
|
2642
|
+
.orgs=${this.orgs}
|
|
2643
|
+
.selectedOrgId=${this.selectedOrgId}
|
|
2644
|
+
.globalAdmin=${this.globalAdmin}
|
|
2645
|
+
.sessionCount=${this.sessions.length}
|
|
2646
|
+
></idp-admin-sidebar>
|
|
2647
|
+
</div>
|
|
2648
|
+
`
|
|
2649
|
+
: html``}
|
|
2475
2650
|
<main>${this.renderMainContent()}</main>
|
|
2476
2651
|
</div>
|
|
2477
2652
|
${this.renderDialog()}
|
|
2478
2653
|
${this.toast
|
|
2479
2654
|
? html`
|
|
2480
|
-
<div class="toast ${this.toast.tone}">
|
|
2655
|
+
<div class="toast ${this.toast.tone}" role="status" aria-live="polite">
|
|
2481
2656
|
<idp-icon name=${this.toast.tone === 'ok' ? 'check' : 'alert'} size="14"></idp-icon>
|
|
2482
2657
|
<span style="flex:1">${this.toast.message}</span>
|
|
2483
2658
|
</div>
|
|
@@ -535,7 +535,7 @@ export class IdpAdminSidebar extends DeesElement {
|
|
|
535
535
|
return html`
|
|
536
536
|
<div class="nav-list">
|
|
537
537
|
${items.map((item) => html`
|
|
538
|
-
<button class="nav-item ${item.id === active ? 'active' : ''}" @click=${() => this.setPage(item.id)}>
|
|
538
|
+
<button class="nav-item ${item.id === active ? 'active' : ''}" aria-current=${item.id === active ? 'page' : 'false'} @click=${() => this.setPage(item.id)}>
|
|
539
539
|
<idp-icon name=${item.icon as any} size="14"></idp-icon>
|
|
540
540
|
${item.label}
|
|
541
541
|
${item.badge ? html`<idp-badge variant="accent" style="margin-left:auto">${item.badge}</idp-badge>` : html``}
|
|
@@ -604,13 +604,13 @@ export class IdpAdminSidebar extends DeesElement {
|
|
|
604
604
|
<span class="logo-text">idp.global</span>
|
|
605
605
|
</div>
|
|
606
606
|
</div>
|
|
607
|
-
<
|
|
607
|
+
<nav class="nav-wrap" aria-label="Console navigation">
|
|
608
608
|
<div class="nav-section"><div class="nav-title">Workspace</div>${this.renderNavGroup(this.workspaceNav, this.page)}</div>
|
|
609
609
|
<div class="nav-section"><div class="nav-title">My Account</div>${this.renderNavGroup(this.accountNav, this.page)}</div>
|
|
610
610
|
<div class="nav-section">
|
|
611
611
|
<div class="nav-title">Organisation</div>
|
|
612
612
|
<div class="org-switch-wrap">
|
|
613
|
-
<button class="org-switch ${this.orgMenuOpen ? 'open' : ''}" @click=${() => { this.orgMenuOpen = !this.orgMenuOpen; }}>
|
|
613
|
+
<button class="org-switch ${this.orgMenuOpen ? 'open' : ''}" aria-haspopup="menu" aria-expanded=${this.orgMenuOpen} @click=${() => { this.orgMenuOpen = !this.orgMenuOpen; }}>
|
|
614
614
|
<span class="org-avatar">${currentOrg.name.slice(0, 2).toUpperCase()}</span>
|
|
615
615
|
<span class="org-name">${currentOrg.name}</span>
|
|
616
616
|
<idp-icon name="chevron-down" size="10"></idp-icon>
|
|
@@ -634,7 +634,7 @@ export class IdpAdminSidebar extends DeesElement {
|
|
|
634
634
|
</div>
|
|
635
635
|
<div class="nav-section"><div class="nav-title">Support</div>${this.renderNavGroup(this.supportNav, this.page)}</div>
|
|
636
636
|
${this.globalAdmin ? html`<div class="nav-section"><div class="nav-title">Global Admin</div>${this.renderNavGroup(this.adminNav, this.page)}</div>` : html``}
|
|
637
|
-
</
|
|
637
|
+
</nav>
|
|
638
638
|
<div class="user-footer">
|
|
639
639
|
<button class="user-trigger ${this.userMenuOpen ? 'open' : ''}" aria-haspopup="menu" aria-expanded=${this.userMenuOpen} @click=${() => { this.userMenuOpen = !this.userMenuOpen; }}>
|
|
640
640
|
<span class="user-avatar">${this.userInitials}</span>
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { DeesElement, html, property, customElement, css, type TemplateResult } from '@design.estate/dees-element';
|
|
2
|
+
import { idpElementStyles } from './tokens.js';
|
|
3
|
+
import './idp-auth-card.js';
|
|
4
|
+
import './idp-option-row.js';
|
|
5
|
+
import './idp-status-icon.js';
|
|
6
|
+
import './idp-button.js';
|
|
7
|
+
import './idp-spinner.js';
|
|
8
|
+
|
|
9
|
+
export type TIdpInviteState =
|
|
10
|
+
| 'loading'
|
|
11
|
+
| 'valid'
|
|
12
|
+
| 'accepted'
|
|
13
|
+
| 'expired'
|
|
14
|
+
| 'cancelled'
|
|
15
|
+
| 'invalid'
|
|
16
|
+
| 'email-mismatch';
|
|
17
|
+
|
|
18
|
+
declare global {
|
|
19
|
+
interface HTMLElementTagNameMap {
|
|
20
|
+
'idp-invite-card': IdpInviteCard;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Invitation landing card. Purely presentational: the host loads the
|
|
26
|
+
* invitation, decides the state, and reacts to the events
|
|
27
|
+
* idp-invite-accept, idp-invite-signin, idp-invite-register,
|
|
28
|
+
* idp-invite-switch-account and idp-invite-continue.
|
|
29
|
+
*/
|
|
30
|
+
@customElement('idp-invite-card')
|
|
31
|
+
export class IdpInviteCard extends DeesElement {
|
|
32
|
+
public static demo = () => html`
|
|
33
|
+
<div style="background: var(--sh-bg, #f5f5f7); padding: 40px; display: flex; flex-wrap: wrap; gap: 24px; justify-content: center;">
|
|
34
|
+
<idp-invite-card state="valid" email="alex@lossless.com" .orgNames=${['Lossless GmbH']} .roles=${['member']} signedIn></idp-invite-card>
|
|
35
|
+
<idp-invite-card state="valid" email="alex@lossless.com" .orgNames=${['Lossless GmbH']}></idp-invite-card>
|
|
36
|
+
<idp-invite-card state="email-mismatch" email="alex@lossless.com" signedIn signedInEmail="sam@other.io"></idp-invite-card>
|
|
37
|
+
<idp-invite-card state="expired" .orgNames=${['Lossless GmbH']}></idp-invite-card>
|
|
38
|
+
<idp-invite-card state="accepted" .orgNames=${['Lossless GmbH']}></idp-invite-card>
|
|
39
|
+
<idp-invite-card state="invalid"></idp-invite-card>
|
|
40
|
+
</div>
|
|
41
|
+
`;
|
|
42
|
+
public static demoGroups = ['idp.global v5 auth'];
|
|
43
|
+
|
|
44
|
+
@property({ type: String, reflect: true })
|
|
45
|
+
public accessor state: TIdpInviteState = 'loading';
|
|
46
|
+
|
|
47
|
+
/** email address the invitation was issued to */
|
|
48
|
+
@property({ type: String })
|
|
49
|
+
public accessor email = '';
|
|
50
|
+
|
|
51
|
+
/** names of the organizations the invitation grants membership in */
|
|
52
|
+
@property({ type: Array })
|
|
53
|
+
public accessor orgNames: string[] = [];
|
|
54
|
+
|
|
55
|
+
/** roles granted on acceptance */
|
|
56
|
+
@property({ type: Array })
|
|
57
|
+
public accessor roles: string[] = [];
|
|
58
|
+
|
|
59
|
+
/** whether a user session exists */
|
|
60
|
+
@property({ type: Boolean })
|
|
61
|
+
public accessor signedIn = false;
|
|
62
|
+
|
|
63
|
+
/** email of the currently signed-in user (shown on email-mismatch) */
|
|
64
|
+
@property({ type: String })
|
|
65
|
+
public accessor signedInEmail = '';
|
|
66
|
+
|
|
67
|
+
/** disables actions while the host performs a request */
|
|
68
|
+
@property({ type: Boolean })
|
|
69
|
+
public accessor busy = false;
|
|
70
|
+
|
|
71
|
+
public static styles = [
|
|
72
|
+
...idpElementStyles,
|
|
73
|
+
css`
|
|
74
|
+
:host {
|
|
75
|
+
display: block;
|
|
76
|
+
font-family: var(--idp-font);
|
|
77
|
+
}
|
|
78
|
+
.status-block {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 14px;
|
|
83
|
+
padding: 18px 0 6px;
|
|
84
|
+
text-align: center;
|
|
85
|
+
}
|
|
86
|
+
.status-text {
|
|
87
|
+
font-size: 13px;
|
|
88
|
+
color: var(--idp-fg-2);
|
|
89
|
+
line-height: 1.5;
|
|
90
|
+
max-width: 280px;
|
|
91
|
+
}
|
|
92
|
+
.detail-rows {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 10px;
|
|
96
|
+
margin-top: 16px;
|
|
97
|
+
}
|
|
98
|
+
.actions {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 10px;
|
|
102
|
+
margin-top: 18px;
|
|
103
|
+
}
|
|
104
|
+
.loading-block {
|
|
105
|
+
display: flex;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
padding: 34px 0;
|
|
108
|
+
}
|
|
109
|
+
`,
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
private get orgLabel(): string {
|
|
113
|
+
if (!this.orgNames.length) return 'an organisation';
|
|
114
|
+
if (this.orgNames.length === 1) return this.orgNames[0];
|
|
115
|
+
return `${this.orgNames[0]} and ${this.orgNames.length - 1} more`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public render(): TemplateResult {
|
|
119
|
+
const renderers: Record<TIdpInviteState, () => { heading: string; description: string; body: TemplateResult }> = {
|
|
120
|
+
loading: () => ({
|
|
121
|
+
heading: 'Checking your invitation…',
|
|
122
|
+
description: '',
|
|
123
|
+
body: html`<div class="loading-block" role="status" aria-label="Loading invitation"><idp-spinner size="30"></idp-spinner></div>`,
|
|
124
|
+
}),
|
|
125
|
+
valid: () => ({
|
|
126
|
+
heading: `Join ${this.orgLabel}`,
|
|
127
|
+
description: `You've been invited to join ${this.orgLabel} on idp.global.`,
|
|
128
|
+
body: html`
|
|
129
|
+
<div class="detail-rows">
|
|
130
|
+
<idp-option-row interactive="false" tileTone="accent" tileSize="30" icon="mail" label="Invitation for" description=${this.email}></idp-option-row>
|
|
131
|
+
${this.roles.length
|
|
132
|
+
? html`<idp-option-row interactive="false" tileTone="accent" tileSize="30" icon="shield" label="You'll join as" description=${this.roles.join(', ')}></idp-option-row>`
|
|
133
|
+
: html``}
|
|
134
|
+
</div>
|
|
135
|
+
<div class="actions">
|
|
136
|
+
${this.signedIn
|
|
137
|
+
? html`<idp-button variant="accent" shape="rect" block ?loading=${this.busy} @click=${() => this.emit('idp-invite-accept')}>Accept invitation</idp-button>`
|
|
138
|
+
: html`
|
|
139
|
+
<idp-button variant="accent" shape="rect" block ?disabled=${this.busy} @click=${() => this.emit('idp-invite-signin')}>Sign in to accept</idp-button>
|
|
140
|
+
<idp-button variant="outline" shape="rect" block ?disabled=${this.busy} @click=${() => this.emit('idp-invite-register')}>Create an account</idp-button>
|
|
141
|
+
`}
|
|
142
|
+
</div>
|
|
143
|
+
`,
|
|
144
|
+
}),
|
|
145
|
+
accepted: () => ({
|
|
146
|
+
heading: `Welcome to ${this.orgLabel}`,
|
|
147
|
+
description: '',
|
|
148
|
+
body: html`
|
|
149
|
+
<div class="status-block">
|
|
150
|
+
<idp-status-icon tone="ok" icon="check"></idp-status-icon>
|
|
151
|
+
<div class="status-text">Your invitation has been accepted. You're now a member of ${this.orgLabel}.</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="actions">
|
|
154
|
+
<idp-button variant="accent" shape="rect" block @click=${() => this.emit('idp-invite-continue')}>Continue to dashboard</idp-button>
|
|
155
|
+
</div>
|
|
156
|
+
`,
|
|
157
|
+
}),
|
|
158
|
+
expired: () => ({
|
|
159
|
+
heading: 'Invitation expired',
|
|
160
|
+
description: '',
|
|
161
|
+
body: html`
|
|
162
|
+
<div class="status-block">
|
|
163
|
+
<idp-status-icon tone="warn" icon="clock"></idp-status-icon>
|
|
164
|
+
<div class="status-text">This invitation to ${this.orgLabel} is no longer valid. Ask an organisation admin to send you a new one.</div>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="actions">
|
|
167
|
+
<idp-button variant="outline" shape="rect" block @click=${() => this.emit('idp-invite-continue')}>Go to idp.global</idp-button>
|
|
168
|
+
</div>
|
|
169
|
+
`,
|
|
170
|
+
}),
|
|
171
|
+
cancelled: () => ({
|
|
172
|
+
heading: 'Invitation withdrawn',
|
|
173
|
+
description: '',
|
|
174
|
+
body: html`
|
|
175
|
+
<div class="status-block">
|
|
176
|
+
<idp-status-icon tone="warn" icon="ban"></idp-status-icon>
|
|
177
|
+
<div class="status-text">This invitation was withdrawn by the organisation. Ask an organisation admin to send you a new one if you believe this is a mistake.</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="actions">
|
|
180
|
+
<idp-button variant="outline" shape="rect" block @click=${() => this.emit('idp-invite-continue')}>Go to idp.global</idp-button>
|
|
181
|
+
</div>
|
|
182
|
+
`,
|
|
183
|
+
}),
|
|
184
|
+
invalid: () => ({
|
|
185
|
+
heading: 'Invitation not found',
|
|
186
|
+
description: '',
|
|
187
|
+
body: html`
|
|
188
|
+
<div class="status-block">
|
|
189
|
+
<idp-status-icon tone="error" icon="x"></idp-status-icon>
|
|
190
|
+
<div class="status-text">This invitation link is invalid or was already used. Check that you opened the full link from your email.</div>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="actions">
|
|
193
|
+
<idp-button variant="outline" shape="rect" block @click=${() => this.emit('idp-invite-continue')}>Go to idp.global</idp-button>
|
|
194
|
+
</div>
|
|
195
|
+
`,
|
|
196
|
+
}),
|
|
197
|
+
'email-mismatch': () => ({
|
|
198
|
+
heading: 'Different account signed in',
|
|
199
|
+
description: '',
|
|
200
|
+
body: html`
|
|
201
|
+
<div class="status-block">
|
|
202
|
+
<idp-status-icon tone="warn" icon="user-x"></idp-status-icon>
|
|
203
|
+
<div class="status-text">
|
|
204
|
+
This invitation was sent to <strong>${this.email}</strong>, but you're signed in as
|
|
205
|
+
<strong>${this.signedInEmail}</strong>. Switch accounts to accept it.
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="actions">
|
|
209
|
+
<idp-button variant="accent" shape="rect" block ?disabled=${this.busy} @click=${() => this.emit('idp-invite-switch-account')}>Switch account</idp-button>
|
|
210
|
+
<idp-button variant="outline" shape="rect" block ?disabled=${this.busy} @click=${() => this.emit('idp-invite-continue')}>Not now</idp-button>
|
|
211
|
+
</div>
|
|
212
|
+
`,
|
|
213
|
+
}),
|
|
214
|
+
};
|
|
215
|
+
const view = renderers[this.state]();
|
|
216
|
+
return html`
|
|
217
|
+
<idp-auth-card heading=${view.heading} description=${view.description}>
|
|
218
|
+
${view.body}
|
|
219
|
+
${this.signedIn && this.signedInEmail && this.state === 'valid'
|
|
220
|
+
? html`<span slot="footer">Signed in as ${this.signedInEmail}</span>`
|
|
221
|
+
: html``}
|
|
222
|
+
</idp-auth-card>
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private emit(eventName: string) {
|
|
227
|
+
this.dispatchEvent(new CustomEvent(eventName, { bubbles: true, composed: true }));
|
|
228
|
+
}
|
|
229
|
+
}
|
package/ts_web/elements/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export * from './idp-signed-in-pill.js';
|
|
|
22
22
|
export * from './idp-app-pair.js';
|
|
23
23
|
export * from './idp-auth-card.js';
|
|
24
24
|
export * from './idp-consent-card.js';
|
|
25
|
+
export * from './idp-invite-card.js';
|
|
25
26
|
export * from './idp-approval-card.js';
|
|
26
27
|
export * from './idp-mobile-frame.js';
|
|
27
28
|
export * from './idp-inbox-preview.js';
|