@devsym/graph-toolkit-react 0.1.0-next.2
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/LICENSE +24 -0
- package/dist/components/Person/Person.d.ts +7 -0
- package/dist/components/Person/Person.d.ts.map +1 -0
- package/dist/components/Person/Person.types.d.ts +35 -0
- package/dist/components/Person/Person.types.d.ts.map +1 -0
- package/dist/components/Person/index.d.ts +6 -0
- package/dist/components/Person/index.d.ts.map +1 -0
- package/dist/hooks/useGraphClient.d.ts +9 -0
- package/dist/hooks/useGraphClient.d.ts.map +1 -0
- package/dist/hooks/usePersonData.d.ts +22 -0
- package/dist/hooks/usePersonData.d.ts.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +465 -0
- package/dist/providers/IProvider.d.ts +31 -0
- package/dist/providers/IProvider.d.ts.map +1 -0
- package/dist/providers/MockProvider.d.ts +21 -0
- package/dist/providers/MockProvider.d.ts.map +1 -0
- package/dist/providers/MsalBrowserProvider.d.ts +23 -0
- package/dist/providers/MsalBrowserProvider.d.ts.map +1 -0
- package/dist/providers/ProviderContext.d.ts +19 -0
- package/dist/providers/ProviderContext.d.ts.map +1 -0
- package/dist/providers/TeamsHostedProvider.d.ts +59 -0
- package/dist/providers/TeamsHostedProvider.d.ts.map +1 -0
- package/dist/utils/graph.d.ts +14 -0
- package/dist/utils/graph.d.ts.map +1 -0
- package/package.json +94 -0
- package/readme.md +355 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft and Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
Note: Usage of the assets referenced in Office UI Fabric files is subject to
|
|
24
|
+
the terms listed at https://aka.ms/fabric-assets-license
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Person.d.ts","sourceRoot":"","sources":["../../../src/components/Person/Person.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AA2C7C,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAuFxC,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Person component types
|
|
3
|
+
*/
|
|
4
|
+
import type { CSSProperties } from 'react';
|
|
5
|
+
import { User } from '@microsoft/microsoft-graph-types';
|
|
6
|
+
export type PersonView = 'avatar' | 'oneline' | 'twolines' | 'threelines';
|
|
7
|
+
export type PersonSize = 'small' | 'medium' | 'large' | 'extra-large';
|
|
8
|
+
export type PersonTextAlignment = 'start' | 'center';
|
|
9
|
+
export type PersonTextPosition = 'before' | 'after' | 'below';
|
|
10
|
+
export interface PersonDetails {
|
|
11
|
+
displayName?: string;
|
|
12
|
+
mail?: string;
|
|
13
|
+
jobTitle?: string;
|
|
14
|
+
department?: string;
|
|
15
|
+
officeLocation?: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
userPrincipalName?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PersonProps {
|
|
20
|
+
userId?: string;
|
|
21
|
+
userPrincipalName?: string;
|
|
22
|
+
email?: string;
|
|
23
|
+
personDetails?: PersonDetails;
|
|
24
|
+
view?: PersonView;
|
|
25
|
+
showPresence?: boolean;
|
|
26
|
+
avatarSize?: PersonSize;
|
|
27
|
+
textAlignment?: PersonTextAlignment;
|
|
28
|
+
textPosition?: PersonTextPosition;
|
|
29
|
+
numericSize?: number;
|
|
30
|
+
fetchImage?: boolean;
|
|
31
|
+
onClick?: (person: User) => void;
|
|
32
|
+
className?: string;
|
|
33
|
+
style?: CSSProperties;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=Person.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Person.types.d.ts","sourceRoot":"","sources":["../../../src/components/Person/Person.types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,kCAAkC,CAAC;AAExD,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,YAAY,CAAC;AAC1E,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,aAAa,CAAC;AACtE,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,QAAQ,CAAC;AACrD,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;AAE9D,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,WAAW;IAE1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,aAAa,CAAC,EAAE,aAAa,CAAC;IAG9B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,UAAU,CAAC;IAGxB,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,UAAU,CAAC,EAAE,OAAO,CAAC;IAGrB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,KAAK,IAAI,CAAC;IAGjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Person/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to access Graph Client
|
|
3
|
+
*/
|
|
4
|
+
import { Client } from '@microsoft/microsoft-graph-client';
|
|
5
|
+
/**
|
|
6
|
+
* Hook to get a configured Graph Client instance
|
|
7
|
+
*/
|
|
8
|
+
export declare const useGraphClient: () => Client | null;
|
|
9
|
+
//# sourceMappingURL=useGraphClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGraphClient.d.ts","sourceRoot":"","sources":["../../src/hooks/useGraphClient.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAI3D;;GAEG;AACH,eAAO,MAAM,cAAc,QAAO,MAAM,GAAG,IAS1C,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to fetch person data from Microsoft Graph
|
|
3
|
+
*/
|
|
4
|
+
import { User, Presence } from '@microsoft/microsoft-graph-types';
|
|
5
|
+
export interface PersonData {
|
|
6
|
+
user: User | null;
|
|
7
|
+
presence: Presence | null;
|
|
8
|
+
photoUrl: string | null;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
error: Error | null;
|
|
11
|
+
}
|
|
12
|
+
export interface UsePersonDataOptions {
|
|
13
|
+
userId?: string;
|
|
14
|
+
userPrincipalName?: string;
|
|
15
|
+
fetchPresence?: boolean;
|
|
16
|
+
fetchPhoto?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Fetch person data from Microsoft Graph
|
|
20
|
+
*/
|
|
21
|
+
export declare const usePersonData: (options: UsePersonDataOptions) => PersonData;
|
|
22
|
+
//# sourceMappingURL=usePersonData.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePersonData.d.ts","sourceRoot":"","sources":["../../src/hooks/usePersonData.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAKlE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,SAAS,oBAAoB,KAAG,UAgK7D,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph Toolkit React - React Components
|
|
3
|
+
*
|
|
4
|
+
* React components for Microsoft Graph powered by Fluent UI
|
|
5
|
+
*/
|
|
6
|
+
export * from './components/Person';
|
|
7
|
+
export * from './providers/IProvider';
|
|
8
|
+
export * from './providers/ProviderContext';
|
|
9
|
+
export * from './providers/MockProvider';
|
|
10
|
+
export * from './providers/MsalBrowserProvider';
|
|
11
|
+
export * from './providers/TeamsHostedProvider';
|
|
12
|
+
export * from './hooks/useGraphClient';
|
|
13
|
+
export * from './hooks/usePersonData';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,cAAc,qBAAqB,CAAC;AAGpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,iCAAiC,CAAC;AAChD,cAAc,iCAAiC,CAAC;AAGhD,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const w=require("react/jsx-runtime"),O=require("@fluentui/react-components"),d=require("react"),q=require("@microsoft/microsoft-graph-client");var o=(t=>(t.Loading="Loading",t.SignedOut="SignedOut",t.SignedIn="SignedIn",t))(o||{});const P=d.createContext({provider:null,state:o.Loading}),G=({provider:t,children:e})=>{const s=d.useCallback(()=>t?.getState?.()??o.Loading,[t]),r=d.useCallback(i=>(t.onStateChanged?.(i),()=>{const c=t;typeof c.removeStateChangedHandler=="function"&&c.removeStateChangedHandler(i)}),[t]),n=d.useSyncExternalStore(r,s,s),a=d.useMemo(()=>({provider:t,state:n}),[t,n]);return w.jsx(P.Provider,{value:a,children:e})},I=()=>{const{provider:t}=d.useContext(P);return t},b=()=>{const{state:t}=d.useContext(P);return t},L=t=>q.Client.init({authProvider:async e=>{try{const s=await t.getAccessToken();e(null,s)}catch(s){e(s,null)}}}),z=t=>{const e=t.trim().split(" ");return e.length===1?e[0].substring(0,2).toUpperCase():(e[0][0]+e[e.length-1][0]).toUpperCase()},U=()=>{const t=I(),e=b();return d.useMemo(()=>!t||e!=="SignedIn"?null:L(t),[t,e])};class C{state=o.SignedOut;listeners=[];constructor(e){e?.autoSignIn&&(this.state=o.SignedIn)}getAccessToken(){return this.state!==o.SignedIn?Promise.reject(new Error("Not signed in")):Promise.resolve("mock-token")}async login(){this.state=o.SignedIn,this.emit()}async logout(){this.state=o.SignedOut,this.emit()}getState(){return this.state}onStateChanged(e){this.listeners.push(e)}removeStateChangedHandler(e){this.listeners=this.listeners.filter(s=>s!==e)}emit(){for(const e of this.listeners)try{e()}catch{}}}const B=()=>new C({autoSignIn:!0}),j=t=>{const e=U(),s=I(),r=b(),{userId:n,userPrincipalName:a,fetchPresence:i=!1,fetchPhoto:c=!0}=t,[f,p]=d.useState({user:null,presence:null,photoUrl:null,loading:!0,error:null});return d.useEffect(()=>{let h=!1;return(async()=>{if(s instanceof C){const l={id:"00000000-0000-0000-0000-000000000000",displayName:"Adele Vance",userPrincipalName:"adelev@contoso.com",jobTitle:"Product Manager",department:"Marketing",officeLocation:"19/3106",mail:"adelev@contoso.com"},u={availability:"Available",activity:"Available",id:l.id,odataType:"#microsoft.graph.presence"};h||p({user:l,presence:i?u:null,photoUrl:c?"https://ui-avatars.com/api/?name=Adele+Vance&background=0D8ABC&color=ffffff":null,loading:!1,error:null});return}if(r!=="SignedIn"){h||p({user:null,presence:null,photoUrl:null,loading:!1,error:r==="SignedOut"?null:new Error("Provider not signed in")});return}if(!e){h||p(l=>({...l,loading:!0}));return}const m=n||a;if(!m){h||p({user:null,presence:null,photoUrl:null,loading:!1,error:null});return}try{p(g=>({...g,loading:!0,error:null}));const l=m.toLowerCase()==="me",u=await e.api(l?"/me":`/users/${m}`).select("id,displayName,jobTitle,mail,department,officeLocation,userPrincipalName").get();if(h)return;let S=null,k=null;if(i&&u.id)try{S=await e.api(l?"/me/presence":`/users/${u.id}/presence`).get()}catch(g){console.warn("Failed to fetch presence:",g)}if(c&&u.id)try{const g=await e.api(l?"/me/photo/$value":`/users/${u.id}/photo/$value`).get();k=URL.createObjectURL(g)}catch(g){console.warn("Failed to fetch photo:",g)}h||p({user:u,presence:S,photoUrl:k,loading:!1,error:null})}catch(l){h||p({user:null,presence:null,photoUrl:null,loading:!1,error:l})}})(),()=>{h=!0}},[e,s,r,n,a,i,c]),f},N=t=>{switch(t){case"small":return"small";case"large":return"large";case"extra-large":return"extra-large";default:return"medium"}},$=t=>{switch(t?.toLowerCase()){case"available":case"availableidle":return"available";case"away":case"berightback":return"away";case"busy":case"busyidle":case"donotdisturb":return"busy";default:return"offline"}},D=({userId:t,userPrincipalName:e,email:s,personDetails:r,view:n="oneline",showPresence:a=!1,avatarSize:i="medium",textAlignment:c,textPosition:f,numericSize:p,fetchImage:h=!0,onClick:y,className:m,style:l})=>{const{user:u,presence:S,photoUrl:k,loading:g}=j({userId:r?void 0:t||e||s,fetchPresence:a,fetchPhoto:h}),T=r||u,E=p??N(i);if(g)return w.jsx(O.Persona,{size:E,name:"Loading...",textAlignment:c,textPosition:f,className:m,style:l});if(!T)return null;const M=T.displayName||"Unknown User",F=z(M),v={size:E,name:M,textAlignment:c,textPosition:f,avatar:{image:k?{src:k}:void 0,initials:k?void 0:F},className:m,style:l};return a&&S&&(v.presence={status:$(S.availability)}),n!=="avatar"&&((n==="twolines"||n==="threelines")&&(v.secondaryText=T.jobTitle??void 0),n==="threelines"&&(v.tertiaryText=T.department??void 0)),y&&(v.onClick=()=>{u&&y(u)}),w.jsx(O.Persona,{...v})};function H(t){return`See https://aka.ms/msal.js.errors#${t} for details`}class x extends Error{constructor(e,s,r){const n=s||(e?H(e):""),a=n?`${e}: ${n}`:e;super(a),Object.setPrototypeOf(this,x.prototype),this.errorCode=e||"",this.errorMessage=n||"",this.subError=r||"",this.name="AuthError"}setCorrelationId(e){this.correlationId=e}}class A extends x{constructor(e,s,r,n,a,i,c,f){super(e,s,r),Object.setPrototypeOf(this,A.prototype),this.timestamp=n||"",this.traceId=a||"",this.correlationId=i||"",this.claims=c||"",this.name="InteractionRequiredAuthError",this.errorNo=f}}class R{msalInstance;defaultScopes;account=null;state=o.Loading;listeners=[];constructor(e,s){this.msalInstance=e,this.defaultScopes=s}async initialize(){await this.msalInstance.initialize(),await this.msalInstance.handleRedirectPromise();const e=this.msalInstance.getActiveAccount(),s=this.msalInstance.getAllAccounts();this.account=e??s[0]??null,this.account?(this.msalInstance.setActiveAccount(this.account),this.setState(o.SignedIn)):this.setState(o.SignedOut)}async login(){const e={scopes:this.defaultScopes},s=await this.msalInstance.loginPopup(e);this.account=s.account,this.account&&this.msalInstance.setActiveAccount(this.account),this.setState(o.SignedIn)}async logout(){const e=this.msalInstance.getActiveAccount()??this.account??void 0;await this.msalInstance.logoutPopup({account:e}),this.account=null,this.setState(o.SignedOut)}async getAccessToken(e){const s=e&&e.length>0?e:this.defaultScopes,r=this.msalInstance.getActiveAccount()??this.account;if(!r)throw new Error("No active account. User must sign in first.");const n={scopes:s,account:r};try{return(await this.msalInstance.acquireTokenSilent(n)).accessToken}catch(a){if(a instanceof A){const i={scopes:s,account:r};return(await this.msalInstance.acquireTokenPopup(i)).accessToken}throw a}}getState(){return this.state}onStateChanged(e){this.listeners.push(e)}removeStateChangedHandler(e){this.listeners=this.listeners.filter(s=>s!==e)}setState(e){this.state=e,this.emit()}emit(){for(const e of this.listeners)try{e()}catch{}}}const K=6e4;function V(t){const e=t.fetchFn??fetch;return async({ssoToken:s,scopes:r})=>{const n=await e(t.endpoint,{method:"POST",headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json"},body:JSON.stringify({scopes:r})}),a=await n.json().catch(()=>({error:n.statusText||"Token exchange failed"}));if(!n.ok){const i=a?.error||a?.message||n.statusText;throw new Error(`Backend token exchange failed: ${i}`)}if(!a?.accessToken)throw new Error("Backend token exchange response did not include accessToken.");return{accessToken:a.accessToken,expiresAt:a.expiresAt}}}class J{defaultScopes;getTeamsSsoTokenFn;exchangeForGraphTokenFn;refreshSkewMs;state=o.SignedOut;listeners=[];tokenCache=new Map;constructor(e){if(!e.defaultScopes||e.defaultScopes.length===0)throw new Error("TeamsHostedProvider requires at least one default scope.");this.defaultScopes=[...new Set(e.defaultScopes)],this.getTeamsSsoTokenFn=e.getTeamsSsoToken,this.exchangeForGraphTokenFn=e.exchangeForGraphToken,this.refreshSkewMs=e.refreshSkewMs??K}async login(){try{await this.getAccessToken(),this.setState(o.SignedIn)}catch(e){throw this.clearCache(),this.setState(o.SignedOut),e}}async logout(){this.clearCache(),this.setState(o.SignedOut)}async getAccessToken(e){const s=this.normalizeScopes(e),r=this.buildScopeKey(s),n=this.tokenCache.get(r);if(n&&this.isTokenValid(n))return n.accessToken;const a=await this.getTeamsSsoTokenFn(s);if(!a)throw new Error("Teams SSO token acquisition returned empty token.");const i=await this.exchangeForGraphTokenFn({ssoToken:a,scopes:s}),c=this.normalizeExchangedToken(i);return this.tokenCache.set(r,c),this.setState(o.SignedIn),c.accessToken}getState(){return this.state}onStateChanged(e){this.listeners.push(e)}removeStateChangedHandler(e){this.listeners=this.listeners.filter(s=>s!==e)}normalizeScopes(e){const s=e&&e.length>0?e:this.defaultScopes,r=[...new Set(s.filter(Boolean))];if(r.length===0)throw new Error("At least one scope is required to request a Graph token.");return r}buildScopeKey(e){return[...e].sort().join(" ")}isTokenValid(e){return e.expiresAtMs?Date.now()+this.refreshSkewMs<e.expiresAtMs:!0}normalizeExchangedToken(e){if(typeof e=="string"){if(!e)throw new Error("Graph token exchange returned empty access token.");return{accessToken:e}}if(!e.accessToken)throw new Error("Graph token exchange returned empty access token.");return{accessToken:e.accessToken,expiresAtMs:this.toEpochMs(e.expiresAt)}}toEpochMs(e){if(e)return e instanceof Date?e.getTime():e}clearCache(){this.tokenCache.clear()}setState(e){this.state!==e&&(this.state=e,this.emit())}emit(){for(const e of this.listeners)try{e()}catch{}}}exports.GraphProvider=G;exports.MockProvider=C;exports.MsalBrowserProvider=R;exports.Person=D;exports.ProviderState=o;exports.TeamsHostedProvider=J;exports.createBackendTokenExchange=V;exports.createMockSignedInProvider=B;exports.useGraphClient=U;exports.usePersonData=j;exports.useProvider=I;exports.useProviderState=b;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { jsx as w } from "react/jsx-runtime";
|
|
2
|
+
import { Persona as C } from "@fluentui/react-components";
|
|
3
|
+
import { createContext as j, useCallback as x, useSyncExternalStore as G, useMemo as E, useContext as M, useState as q, useEffect as z } from "react";
|
|
4
|
+
import { Client as N } from "@microsoft/microsoft-graph-client";
|
|
5
|
+
var o = /* @__PURE__ */ ((t) => (t.Loading = "Loading", t.SignedOut = "SignedOut", t.SignedIn = "SignedIn", t))(o || {});
|
|
6
|
+
const v = j({
|
|
7
|
+
provider: null,
|
|
8
|
+
state: o.Loading
|
|
9
|
+
}), Y = ({ provider: t, children: e }) => {
|
|
10
|
+
const s = x(
|
|
11
|
+
() => t?.getState?.() ?? o.Loading,
|
|
12
|
+
[t]
|
|
13
|
+
), r = x(
|
|
14
|
+
(i) => (t.onStateChanged?.(i), () => {
|
|
15
|
+
const c = t;
|
|
16
|
+
typeof c.removeStateChangedHandler == "function" && c.removeStateChangedHandler(i);
|
|
17
|
+
}),
|
|
18
|
+
[t]
|
|
19
|
+
), n = G(r, s, s), a = E(() => ({ provider: t, state: n }), [t, n]);
|
|
20
|
+
return /* @__PURE__ */ w(v.Provider, { value: a, children: e });
|
|
21
|
+
}, U = () => {
|
|
22
|
+
const { provider: t } = M(v);
|
|
23
|
+
return t;
|
|
24
|
+
}, O = () => {
|
|
25
|
+
const { state: t } = M(v);
|
|
26
|
+
return t;
|
|
27
|
+
}, $ = (t) => N.init({
|
|
28
|
+
authProvider: async (e) => {
|
|
29
|
+
try {
|
|
30
|
+
const s = await t.getAccessToken();
|
|
31
|
+
e(null, s);
|
|
32
|
+
} catch (s) {
|
|
33
|
+
e(s, null);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}), B = (t) => {
|
|
37
|
+
const e = t.trim().split(" ");
|
|
38
|
+
return e.length === 1 ? e[0].substring(0, 2).toUpperCase() : (e[0][0] + e[e.length - 1][0]).toUpperCase();
|
|
39
|
+
}, D = () => {
|
|
40
|
+
const t = U(), e = O();
|
|
41
|
+
return E(() => !t || e !== "SignedIn" ? null : $(t), [t, e]);
|
|
42
|
+
};
|
|
43
|
+
class F {
|
|
44
|
+
state = o.SignedOut;
|
|
45
|
+
listeners = [];
|
|
46
|
+
constructor(e) {
|
|
47
|
+
e?.autoSignIn && (this.state = o.SignedIn);
|
|
48
|
+
}
|
|
49
|
+
getAccessToken() {
|
|
50
|
+
return this.state !== o.SignedIn ? Promise.reject(new Error("Not signed in")) : Promise.resolve("mock-token");
|
|
51
|
+
}
|
|
52
|
+
async login() {
|
|
53
|
+
this.state = o.SignedIn, this.emit();
|
|
54
|
+
}
|
|
55
|
+
async logout() {
|
|
56
|
+
this.state = o.SignedOut, this.emit();
|
|
57
|
+
}
|
|
58
|
+
getState() {
|
|
59
|
+
return this.state;
|
|
60
|
+
}
|
|
61
|
+
onStateChanged(e) {
|
|
62
|
+
this.listeners.push(e);
|
|
63
|
+
}
|
|
64
|
+
removeStateChangedHandler(e) {
|
|
65
|
+
this.listeners = this.listeners.filter((s) => s !== e);
|
|
66
|
+
}
|
|
67
|
+
emit() {
|
|
68
|
+
for (const e of this.listeners)
|
|
69
|
+
try {
|
|
70
|
+
e();
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const Z = () => new F({ autoSignIn: !0 }), H = (t) => {
|
|
76
|
+
const e = D(), s = U(), r = O(), { userId: n, userPrincipalName: a, fetchPresence: i = !1, fetchPhoto: c = !0 } = t, [p, d] = q({
|
|
77
|
+
user: null,
|
|
78
|
+
presence: null,
|
|
79
|
+
photoUrl: null,
|
|
80
|
+
loading: !0,
|
|
81
|
+
error: null
|
|
82
|
+
});
|
|
83
|
+
return z(() => {
|
|
84
|
+
let h = !1;
|
|
85
|
+
return (async () => {
|
|
86
|
+
if (s instanceof F) {
|
|
87
|
+
const l = {
|
|
88
|
+
id: "00000000-0000-0000-0000-000000000000",
|
|
89
|
+
displayName: "Adele Vance",
|
|
90
|
+
userPrincipalName: "adelev@contoso.com",
|
|
91
|
+
jobTitle: "Product Manager",
|
|
92
|
+
department: "Marketing",
|
|
93
|
+
officeLocation: "19/3106",
|
|
94
|
+
mail: "adelev@contoso.com"
|
|
95
|
+
}, u = {
|
|
96
|
+
availability: "Available",
|
|
97
|
+
activity: "Available",
|
|
98
|
+
id: l.id,
|
|
99
|
+
odataType: "#microsoft.graph.presence"
|
|
100
|
+
};
|
|
101
|
+
h || d({
|
|
102
|
+
user: l,
|
|
103
|
+
presence: i ? u : null,
|
|
104
|
+
photoUrl: c ? "https://ui-avatars.com/api/?name=Adele+Vance&background=0D8ABC&color=ffffff" : null,
|
|
105
|
+
loading: !1,
|
|
106
|
+
error: null
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (r !== "SignedIn") {
|
|
111
|
+
h || d({
|
|
112
|
+
user: null,
|
|
113
|
+
presence: null,
|
|
114
|
+
photoUrl: null,
|
|
115
|
+
loading: !1,
|
|
116
|
+
error: r === "SignedOut" ? null : new Error("Provider not signed in")
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!e) {
|
|
121
|
+
h || d((l) => ({ ...l, loading: !0 }));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const g = n || a;
|
|
125
|
+
if (!g) {
|
|
126
|
+
h || d({
|
|
127
|
+
user: null,
|
|
128
|
+
presence: null,
|
|
129
|
+
photoUrl: null,
|
|
130
|
+
loading: !1,
|
|
131
|
+
error: null
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
d((f) => ({ ...f, loading: !0, error: null }));
|
|
137
|
+
const l = g.toLowerCase() === "me", u = await e.api(l ? "/me" : `/users/${g}`).select("id,displayName,jobTitle,mail,department,officeLocation,userPrincipalName").get();
|
|
138
|
+
if (h) return;
|
|
139
|
+
let m = null, S = null;
|
|
140
|
+
if (i && u.id)
|
|
141
|
+
try {
|
|
142
|
+
m = await e.api(l ? "/me/presence" : `/users/${u.id}/presence`).get();
|
|
143
|
+
} catch (f) {
|
|
144
|
+
console.warn("Failed to fetch presence:", f);
|
|
145
|
+
}
|
|
146
|
+
if (c && u.id)
|
|
147
|
+
try {
|
|
148
|
+
const f = await e.api(l ? "/me/photo/$value" : `/users/${u.id}/photo/$value`).get();
|
|
149
|
+
S = URL.createObjectURL(f);
|
|
150
|
+
} catch (f) {
|
|
151
|
+
console.warn("Failed to fetch photo:", f);
|
|
152
|
+
}
|
|
153
|
+
h || d({
|
|
154
|
+
user: u,
|
|
155
|
+
presence: m,
|
|
156
|
+
photoUrl: S,
|
|
157
|
+
loading: !1,
|
|
158
|
+
error: null
|
|
159
|
+
});
|
|
160
|
+
} catch (l) {
|
|
161
|
+
h || d({
|
|
162
|
+
user: null,
|
|
163
|
+
presence: null,
|
|
164
|
+
photoUrl: null,
|
|
165
|
+
loading: !1,
|
|
166
|
+
error: l
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
})(), () => {
|
|
170
|
+
h = !0;
|
|
171
|
+
};
|
|
172
|
+
}, [
|
|
173
|
+
e,
|
|
174
|
+
s,
|
|
175
|
+
r,
|
|
176
|
+
n,
|
|
177
|
+
a,
|
|
178
|
+
i,
|
|
179
|
+
c
|
|
180
|
+
]), p;
|
|
181
|
+
}, R = (t) => {
|
|
182
|
+
switch (t) {
|
|
183
|
+
case "small":
|
|
184
|
+
return "small";
|
|
185
|
+
case "large":
|
|
186
|
+
return "large";
|
|
187
|
+
case "extra-large":
|
|
188
|
+
return "extra-large";
|
|
189
|
+
default:
|
|
190
|
+
return "medium";
|
|
191
|
+
}
|
|
192
|
+
}, K = (t) => {
|
|
193
|
+
switch (t?.toLowerCase()) {
|
|
194
|
+
case "available":
|
|
195
|
+
case "availableidle":
|
|
196
|
+
return "available";
|
|
197
|
+
case "away":
|
|
198
|
+
case "berightback":
|
|
199
|
+
return "away";
|
|
200
|
+
case "busy":
|
|
201
|
+
case "busyidle":
|
|
202
|
+
case "donotdisturb":
|
|
203
|
+
return "busy";
|
|
204
|
+
default:
|
|
205
|
+
return "offline";
|
|
206
|
+
}
|
|
207
|
+
}, ee = ({
|
|
208
|
+
userId: t,
|
|
209
|
+
userPrincipalName: e,
|
|
210
|
+
email: s,
|
|
211
|
+
personDetails: r,
|
|
212
|
+
view: n = "oneline",
|
|
213
|
+
showPresence: a = !1,
|
|
214
|
+
avatarSize: i = "medium",
|
|
215
|
+
textAlignment: c,
|
|
216
|
+
textPosition: p,
|
|
217
|
+
numericSize: d,
|
|
218
|
+
fetchImage: h = !0,
|
|
219
|
+
onClick: T,
|
|
220
|
+
className: g,
|
|
221
|
+
style: l
|
|
222
|
+
}) => {
|
|
223
|
+
const { user: u, presence: m, photoUrl: S, loading: f } = H({
|
|
224
|
+
userId: r ? void 0 : t || e || s,
|
|
225
|
+
fetchPresence: a,
|
|
226
|
+
fetchPhoto: h
|
|
227
|
+
}), y = r || u, A = d ?? R(i);
|
|
228
|
+
if (f)
|
|
229
|
+
return /* @__PURE__ */ w(
|
|
230
|
+
C,
|
|
231
|
+
{
|
|
232
|
+
size: A,
|
|
233
|
+
name: "Loading...",
|
|
234
|
+
textAlignment: c,
|
|
235
|
+
textPosition: p,
|
|
236
|
+
className: g,
|
|
237
|
+
style: l
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
if (!y)
|
|
241
|
+
return null;
|
|
242
|
+
const b = y.displayName || "Unknown User", L = B(b), k = {
|
|
243
|
+
size: A,
|
|
244
|
+
name: b,
|
|
245
|
+
textAlignment: c,
|
|
246
|
+
textPosition: p,
|
|
247
|
+
avatar: {
|
|
248
|
+
image: S ? { src: S } : void 0,
|
|
249
|
+
initials: S ? void 0 : L
|
|
250
|
+
},
|
|
251
|
+
className: g,
|
|
252
|
+
style: l
|
|
253
|
+
};
|
|
254
|
+
return a && m && (k.presence = {
|
|
255
|
+
status: K(m.availability)
|
|
256
|
+
}), n !== "avatar" && ((n === "twolines" || n === "threelines") && (k.secondaryText = y.jobTitle ?? void 0), n === "threelines" && (k.tertiaryText = y.department ?? void 0)), T && (k.onClick = () => {
|
|
257
|
+
u && T(u);
|
|
258
|
+
}), /* @__PURE__ */ w(C, { ...k });
|
|
259
|
+
};
|
|
260
|
+
function V(t) {
|
|
261
|
+
return `See https://aka.ms/msal.js.errors#${t} for details`;
|
|
262
|
+
}
|
|
263
|
+
class I extends Error {
|
|
264
|
+
constructor(e, s, r) {
|
|
265
|
+
const n = s || (e ? V(e) : ""), a = n ? `${e}: ${n}` : e;
|
|
266
|
+
super(a), Object.setPrototypeOf(this, I.prototype), this.errorCode = e || "", this.errorMessage = n || "", this.subError = r || "", this.name = "AuthError";
|
|
267
|
+
}
|
|
268
|
+
setCorrelationId(e) {
|
|
269
|
+
this.correlationId = e;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
class P extends I {
|
|
273
|
+
constructor(e, s, r, n, a, i, c, p) {
|
|
274
|
+
super(e, s, r), Object.setPrototypeOf(this, P.prototype), this.timestamp = n || "", this.traceId = a || "", this.correlationId = i || "", this.claims = c || "", this.name = "InteractionRequiredAuthError", this.errorNo = p;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
class te {
|
|
278
|
+
msalInstance;
|
|
279
|
+
defaultScopes;
|
|
280
|
+
account = null;
|
|
281
|
+
state = o.Loading;
|
|
282
|
+
listeners = [];
|
|
283
|
+
constructor(e, s) {
|
|
284
|
+
this.msalInstance = e, this.defaultScopes = s;
|
|
285
|
+
}
|
|
286
|
+
async initialize() {
|
|
287
|
+
await this.msalInstance.initialize(), await this.msalInstance.handleRedirectPromise();
|
|
288
|
+
const e = this.msalInstance.getActiveAccount(), s = this.msalInstance.getAllAccounts();
|
|
289
|
+
this.account = e ?? s[0] ?? null, this.account ? (this.msalInstance.setActiveAccount(this.account), this.setState(o.SignedIn)) : this.setState(o.SignedOut);
|
|
290
|
+
}
|
|
291
|
+
async login() {
|
|
292
|
+
const e = { scopes: this.defaultScopes }, s = await this.msalInstance.loginPopup(e);
|
|
293
|
+
this.account = s.account, this.account && this.msalInstance.setActiveAccount(this.account), this.setState(o.SignedIn);
|
|
294
|
+
}
|
|
295
|
+
async logout() {
|
|
296
|
+
const e = this.msalInstance.getActiveAccount() ?? this.account ?? void 0;
|
|
297
|
+
await this.msalInstance.logoutPopup({ account: e }), this.account = null, this.setState(o.SignedOut);
|
|
298
|
+
}
|
|
299
|
+
async getAccessToken(e) {
|
|
300
|
+
const s = e && e.length > 0 ? e : this.defaultScopes, r = this.msalInstance.getActiveAccount() ?? this.account;
|
|
301
|
+
if (!r)
|
|
302
|
+
throw new Error("No active account. User must sign in first.");
|
|
303
|
+
const n = {
|
|
304
|
+
scopes: s,
|
|
305
|
+
account: r
|
|
306
|
+
};
|
|
307
|
+
try {
|
|
308
|
+
return (await this.msalInstance.acquireTokenSilent(n)).accessToken;
|
|
309
|
+
} catch (a) {
|
|
310
|
+
if (a instanceof P) {
|
|
311
|
+
const i = {
|
|
312
|
+
scopes: s,
|
|
313
|
+
account: r
|
|
314
|
+
};
|
|
315
|
+
return (await this.msalInstance.acquireTokenPopup(i)).accessToken;
|
|
316
|
+
}
|
|
317
|
+
throw a;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
getState() {
|
|
321
|
+
return this.state;
|
|
322
|
+
}
|
|
323
|
+
onStateChanged(e) {
|
|
324
|
+
this.listeners.push(e);
|
|
325
|
+
}
|
|
326
|
+
removeStateChangedHandler(e) {
|
|
327
|
+
this.listeners = this.listeners.filter((s) => s !== e);
|
|
328
|
+
}
|
|
329
|
+
setState(e) {
|
|
330
|
+
this.state = e, this.emit();
|
|
331
|
+
}
|
|
332
|
+
emit() {
|
|
333
|
+
for (const e of this.listeners)
|
|
334
|
+
try {
|
|
335
|
+
e();
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const J = 6e4;
|
|
341
|
+
function se(t) {
|
|
342
|
+
const e = t.fetchFn ?? fetch;
|
|
343
|
+
return async ({ ssoToken: s, scopes: r }) => {
|
|
344
|
+
const n = await e(t.endpoint, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: {
|
|
347
|
+
Authorization: `Bearer ${s}`,
|
|
348
|
+
"Content-Type": "application/json"
|
|
349
|
+
},
|
|
350
|
+
body: JSON.stringify({ scopes: r })
|
|
351
|
+
}), a = await n.json().catch(() => ({ error: n.statusText || "Token exchange failed" }));
|
|
352
|
+
if (!n.ok) {
|
|
353
|
+
const i = a?.error || a?.message || n.statusText;
|
|
354
|
+
throw new Error(`Backend token exchange failed: ${i}`);
|
|
355
|
+
}
|
|
356
|
+
if (!a?.accessToken)
|
|
357
|
+
throw new Error("Backend token exchange response did not include accessToken.");
|
|
358
|
+
return {
|
|
359
|
+
accessToken: a.accessToken,
|
|
360
|
+
expiresAt: a.expiresAt
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
class ne {
|
|
365
|
+
defaultScopes;
|
|
366
|
+
getTeamsSsoTokenFn;
|
|
367
|
+
exchangeForGraphTokenFn;
|
|
368
|
+
refreshSkewMs;
|
|
369
|
+
state = o.SignedOut;
|
|
370
|
+
listeners = [];
|
|
371
|
+
tokenCache = /* @__PURE__ */ new Map();
|
|
372
|
+
constructor(e) {
|
|
373
|
+
if (!e.defaultScopes || e.defaultScopes.length === 0)
|
|
374
|
+
throw new Error("TeamsHostedProvider requires at least one default scope.");
|
|
375
|
+
this.defaultScopes = [...new Set(e.defaultScopes)], this.getTeamsSsoTokenFn = e.getTeamsSsoToken, this.exchangeForGraphTokenFn = e.exchangeForGraphToken, this.refreshSkewMs = e.refreshSkewMs ?? J;
|
|
376
|
+
}
|
|
377
|
+
async login() {
|
|
378
|
+
try {
|
|
379
|
+
await this.getAccessToken(), this.setState(o.SignedIn);
|
|
380
|
+
} catch (e) {
|
|
381
|
+
throw this.clearCache(), this.setState(o.SignedOut), e;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async logout() {
|
|
385
|
+
this.clearCache(), this.setState(o.SignedOut);
|
|
386
|
+
}
|
|
387
|
+
async getAccessToken(e) {
|
|
388
|
+
const s = this.normalizeScopes(e), r = this.buildScopeKey(s), n = this.tokenCache.get(r);
|
|
389
|
+
if (n && this.isTokenValid(n))
|
|
390
|
+
return n.accessToken;
|
|
391
|
+
const a = await this.getTeamsSsoTokenFn(s);
|
|
392
|
+
if (!a)
|
|
393
|
+
throw new Error("Teams SSO token acquisition returned empty token.");
|
|
394
|
+
const i = await this.exchangeForGraphTokenFn({
|
|
395
|
+
ssoToken: a,
|
|
396
|
+
scopes: s
|
|
397
|
+
}), c = this.normalizeExchangedToken(i);
|
|
398
|
+
return this.tokenCache.set(r, c), this.setState(o.SignedIn), c.accessToken;
|
|
399
|
+
}
|
|
400
|
+
getState() {
|
|
401
|
+
return this.state;
|
|
402
|
+
}
|
|
403
|
+
onStateChanged(e) {
|
|
404
|
+
this.listeners.push(e);
|
|
405
|
+
}
|
|
406
|
+
removeStateChangedHandler(e) {
|
|
407
|
+
this.listeners = this.listeners.filter((s) => s !== e);
|
|
408
|
+
}
|
|
409
|
+
normalizeScopes(e) {
|
|
410
|
+
const s = e && e.length > 0 ? e : this.defaultScopes, r = [...new Set(s.filter(Boolean))];
|
|
411
|
+
if (r.length === 0)
|
|
412
|
+
throw new Error("At least one scope is required to request a Graph token.");
|
|
413
|
+
return r;
|
|
414
|
+
}
|
|
415
|
+
buildScopeKey(e) {
|
|
416
|
+
return [...e].sort().join(" ");
|
|
417
|
+
}
|
|
418
|
+
isTokenValid(e) {
|
|
419
|
+
return e.expiresAtMs ? Date.now() + this.refreshSkewMs < e.expiresAtMs : !0;
|
|
420
|
+
}
|
|
421
|
+
normalizeExchangedToken(e) {
|
|
422
|
+
if (typeof e == "string") {
|
|
423
|
+
if (!e)
|
|
424
|
+
throw new Error("Graph token exchange returned empty access token.");
|
|
425
|
+
return { accessToken: e };
|
|
426
|
+
}
|
|
427
|
+
if (!e.accessToken)
|
|
428
|
+
throw new Error("Graph token exchange returned empty access token.");
|
|
429
|
+
return {
|
|
430
|
+
accessToken: e.accessToken,
|
|
431
|
+
expiresAtMs: this.toEpochMs(e.expiresAt)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
toEpochMs(e) {
|
|
435
|
+
if (e)
|
|
436
|
+
return e instanceof Date ? e.getTime() : e;
|
|
437
|
+
}
|
|
438
|
+
clearCache() {
|
|
439
|
+
this.tokenCache.clear();
|
|
440
|
+
}
|
|
441
|
+
setState(e) {
|
|
442
|
+
this.state !== e && (this.state = e, this.emit());
|
|
443
|
+
}
|
|
444
|
+
emit() {
|
|
445
|
+
for (const e of this.listeners)
|
|
446
|
+
try {
|
|
447
|
+
e();
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
export {
|
|
453
|
+
Y as GraphProvider,
|
|
454
|
+
F as MockProvider,
|
|
455
|
+
te as MsalBrowserProvider,
|
|
456
|
+
ee as Person,
|
|
457
|
+
o as ProviderState,
|
|
458
|
+
ne as TeamsHostedProvider,
|
|
459
|
+
se as createBackendTokenExchange,
|
|
460
|
+
Z as createMockSignedInProvider,
|
|
461
|
+
D as useGraphClient,
|
|
462
|
+
H as usePersonData,
|
|
463
|
+
U as useProvider,
|
|
464
|
+
O as useProviderState
|
|
465
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider interface for Microsoft Graph authentication
|
|
3
|
+
*/
|
|
4
|
+
export declare enum ProviderState {
|
|
5
|
+
Loading = "Loading",
|
|
6
|
+
SignedOut = "SignedOut",
|
|
7
|
+
SignedIn = "SignedIn"
|
|
8
|
+
}
|
|
9
|
+
export interface IProvider {
|
|
10
|
+
/**
|
|
11
|
+
* Get an access token for the specified scopes
|
|
12
|
+
*/
|
|
13
|
+
getAccessToken(scopes?: string[]): Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* Initiate login flow
|
|
16
|
+
*/
|
|
17
|
+
login(): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Initiate logout flow
|
|
20
|
+
*/
|
|
21
|
+
logout(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Get the current provider state
|
|
24
|
+
*/
|
|
25
|
+
getState(): ProviderState;
|
|
26
|
+
/**
|
|
27
|
+
* Event handler for state changes
|
|
28
|
+
*/
|
|
29
|
+
onStateChanged?: (handler: () => void) => void;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=IProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IProvider.d.ts","sourceRoot":"","sources":["../../src/providers/IProvider.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,oBAAY,aAAa;IACvB,OAAO,YAAY;IACnB,SAAS,cAAc;IACvB,QAAQ,aAAa;CACtB;AAED,MAAM,WAAW,SAAS;IACxB;;OAEG;IACH,cAAc,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnD;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB;;OAEG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAExB;;OAEG;IACH,QAAQ,IAAI,aAAa,CAAC;IAE1B;;OAEG;IACH,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CAChD"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockProvider - simple in-memory provider for development and tests
|
|
3
|
+
*/
|
|
4
|
+
import { IProvider, ProviderState } from './IProvider';
|
|
5
|
+
export declare class MockProvider implements IProvider {
|
|
6
|
+
private state;
|
|
7
|
+
private listeners;
|
|
8
|
+
constructor(options?: {
|
|
9
|
+
autoSignIn?: boolean;
|
|
10
|
+
mockUserId?: string;
|
|
11
|
+
});
|
|
12
|
+
getAccessToken(): Promise<string>;
|
|
13
|
+
login(): Promise<void>;
|
|
14
|
+
logout(): Promise<void>;
|
|
15
|
+
getState(): ProviderState;
|
|
16
|
+
onStateChanged(handler: () => void): void;
|
|
17
|
+
removeStateChangedHandler(handler: () => void): void;
|
|
18
|
+
private emit;
|
|
19
|
+
}
|
|
20
|
+
export declare const createMockSignedInProvider: () => MockProvider;
|
|
21
|
+
//# sourceMappingURL=MockProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MockProvider.d.ts","sourceRoot":"","sources":["../../src/providers/MockProvider.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIvD,qBAAa,YAAa,YAAW,SAAS;IAC5C,OAAO,CAAC,KAAK,CAA0C;IACvD,OAAO,CAAC,SAAS,CAAuB;gBAC5B,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAMnE,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAQ3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAK7B,QAAQ,IAAI,aAAa;IAIzB,cAAc,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIzC,yBAAyB,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIpD,OAAO,CAAC,IAAI;CASb;AAED,eAAO,MAAM,0BAA0B,oBAA+C,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native MSAL browser provider for Graph Toolkit React
|
|
3
|
+
*/
|
|
4
|
+
import { PublicClientApplication } from '@azure/msal-browser';
|
|
5
|
+
import { IProvider, ProviderState } from './IProvider';
|
|
6
|
+
export declare class MsalBrowserProvider implements IProvider {
|
|
7
|
+
private readonly msalInstance;
|
|
8
|
+
private readonly defaultScopes;
|
|
9
|
+
private account;
|
|
10
|
+
private state;
|
|
11
|
+
private listeners;
|
|
12
|
+
constructor(msalInstance: PublicClientApplication, defaultScopes: string[]);
|
|
13
|
+
initialize(): Promise<void>;
|
|
14
|
+
login(): Promise<void>;
|
|
15
|
+
logout(): Promise<void>;
|
|
16
|
+
getAccessToken(scopes?: string[]): Promise<string>;
|
|
17
|
+
getState(): ProviderState;
|
|
18
|
+
onStateChanged(handler: () => void): void;
|
|
19
|
+
removeStateChangedHandler(handler: () => void): void;
|
|
20
|
+
private setState;
|
|
21
|
+
private emit;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=MsalBrowserProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MsalBrowserProvider.d.ts","sourceRoot":"","sources":["../../src/providers/MsalBrowserProvider.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAIH,uBAAuB,EAE1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEvD,qBAAa,mBAAoB,YAAW,SAAS;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA0B;IACvD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAW;IACzC,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,KAAK,CAAwC;IACrD,OAAO,CAAC,SAAS,CAAyB;gBAE9B,YAAY,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,EAAE;IAKpE,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAOvB,cAAc,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IA8BxD,QAAQ,IAAI,aAAa;IAIzB,cAAc,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIzC,yBAAyB,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIpD,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,IAAI;CASf"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Context for Graph Provider
|
|
3
|
+
*/
|
|
4
|
+
import React, { ReactNode } from 'react';
|
|
5
|
+
import { IProvider, ProviderState } from './IProvider';
|
|
6
|
+
export interface GraphProviderProps {
|
|
7
|
+
provider: IProvider;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export declare const GraphProvider: React.FC<GraphProviderProps>;
|
|
11
|
+
/**
|
|
12
|
+
* Hook to access the current Graph provider
|
|
13
|
+
*/
|
|
14
|
+
export declare const useProvider: () => IProvider | null;
|
|
15
|
+
/**
|
|
16
|
+
* Hook to access the current provider state
|
|
17
|
+
*/
|
|
18
|
+
export declare const useProviderState: () => ProviderState;
|
|
19
|
+
//# sourceMappingURL=ProviderContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProviderContext.d.ts","sourceRoot":"","sources":["../../src/providers/ProviderContext.tsx"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,EAIZ,SAAS,EAGV,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAYvD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA2BtD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,QAAO,SAAS,GAAG,IAG1C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,gBAAgB,QAAO,aAGnC,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { IProvider, ProviderState } from './IProvider';
|
|
2
|
+
export interface ExchangeForGraphTokenParams {
|
|
3
|
+
ssoToken: string;
|
|
4
|
+
scopes: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface ExchangedGraphToken {
|
|
7
|
+
accessToken: string;
|
|
8
|
+
expiresAt?: number | Date;
|
|
9
|
+
}
|
|
10
|
+
export interface TeamsHostedProviderOptions {
|
|
11
|
+
defaultScopes: string[];
|
|
12
|
+
getTeamsSsoToken: (scopes: string[]) => Promise<string>;
|
|
13
|
+
exchangeForGraphToken: (params: ExchangeForGraphTokenParams) => Promise<string | ExchangedGraphToken>;
|
|
14
|
+
refreshSkewMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface BackendTokenExchangeResponse extends ExchangedGraphToken {
|
|
17
|
+
accessToken: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CreateBackendTokenExchangeOptions {
|
|
20
|
+
endpoint: string;
|
|
21
|
+
fetchFn?: typeof fetch;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Creates a typed callback for exchanging a Teams SSO token with a backend
|
|
25
|
+
* endpoint that performs OBO and returns a Graph token.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createBackendTokenExchange(options: CreateBackendTokenExchangeOptions): ({ ssoToken, scopes }: ExchangeForGraphTokenParams) => Promise<BackendTokenExchangeResponse>;
|
|
28
|
+
/**
|
|
29
|
+
* Teams-hosted auth provider for Graph Toolkit React.
|
|
30
|
+
*
|
|
31
|
+
* This provider does not perform Teams interactive login itself.
|
|
32
|
+
* The consumer application must handle TeamsJS login/token acquisition and
|
|
33
|
+
* provide callbacks for SSO token retrieval and backend Graph token exchange.
|
|
34
|
+
*/
|
|
35
|
+
export declare class TeamsHostedProvider implements IProvider {
|
|
36
|
+
private readonly defaultScopes;
|
|
37
|
+
private readonly getTeamsSsoTokenFn;
|
|
38
|
+
private readonly exchangeForGraphTokenFn;
|
|
39
|
+
private readonly refreshSkewMs;
|
|
40
|
+
private state;
|
|
41
|
+
private listeners;
|
|
42
|
+
private readonly tokenCache;
|
|
43
|
+
constructor(options: TeamsHostedProviderOptions);
|
|
44
|
+
login(): Promise<void>;
|
|
45
|
+
logout(): Promise<void>;
|
|
46
|
+
getAccessToken(scopes?: string[]): Promise<string>;
|
|
47
|
+
getState(): ProviderState;
|
|
48
|
+
onStateChanged(handler: () => void): void;
|
|
49
|
+
removeStateChangedHandler(handler: () => void): void;
|
|
50
|
+
private normalizeScopes;
|
|
51
|
+
private buildScopeKey;
|
|
52
|
+
private isTokenValid;
|
|
53
|
+
private normalizeExchangedToken;
|
|
54
|
+
private toEpochMs;
|
|
55
|
+
private clearCache;
|
|
56
|
+
private setState;
|
|
57
|
+
private emit;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=TeamsHostedProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TeamsHostedProvider.d.ts","sourceRoot":"","sources":["../../src/providers/TeamsHostedProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIvD,MAAM,WAAW,2BAA2B;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACvC,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACxD,qBAAqB,EAAE,CACnB,MAAM,EAAE,2BAA2B,KAClC,OAAO,CAAC,MAAM,GAAG,mBAAmB,CAAC,CAAC;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,4BAA6B,SAAQ,mBAAmB;IACrE,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iCAAiC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AASD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,iCAAiC,IAGnE,sBAAsB,2BAA2B,KAAG,OAAO,CAAC,4BAA4B,CAAC,CA4B1G;AAED;;;;;;GAMG;AACH,qBAAa,mBAAoB,YAAW,SAAS;IACjD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAW;IACzC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAwC;IAC3E,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAEG;IAC3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,KAAK,CAA0C;IACvD,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;gBAEjD,OAAO,EAAE,0BAA0B;IAWzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAWtB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAKvB,cAAc,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BxD,QAAQ,IAAI,aAAa;IAIzB,cAAc,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIzC,yBAAyB,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI;IAIpD,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,uBAAuB;IAmB/B,OAAO,CAAC,SAAS;IAYjB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,IAAI;CASf"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph utility functions
|
|
3
|
+
*/
|
|
4
|
+
import { Client } from '@microsoft/microsoft-graph-client';
|
|
5
|
+
import { IProvider } from '../providers/IProvider';
|
|
6
|
+
/**
|
|
7
|
+
* Create a Graph client from a provider
|
|
8
|
+
*/
|
|
9
|
+
export declare const createGraphClient: (provider: IProvider) => Client;
|
|
10
|
+
/**
|
|
11
|
+
* Extract initials from a display name
|
|
12
|
+
*/
|
|
13
|
+
export declare const getInitials: (displayName?: string) => string;
|
|
14
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/utils/graph.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,SAAS,KAAG,MAWvD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,cAAc,MAAM,KAAG,MASlD,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devsym/graph-toolkit-react",
|
|
3
|
+
"version": "0.1.0-next.2",
|
|
4
|
+
"description": "React components for Microsoft Graph powered by Fluent UI",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "vite build && tsc --emitDeclarationOnly",
|
|
22
|
+
"dev": "vite build --watch",
|
|
23
|
+
"changeset": "changeset",
|
|
24
|
+
"version-packages": "changeset version",
|
|
25
|
+
"publish-packages": "changeset publish",
|
|
26
|
+
"type-check": "tsc --noEmit",
|
|
27
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
28
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
31
|
+
"test:coverage": "vitest run --coverage",
|
|
32
|
+
"prettier:check": "prettier --check \"src/**/*.{ts,tsx}\"",
|
|
33
|
+
"prettier:write": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
34
|
+
"storybook": "storybook dev -p 6006",
|
|
35
|
+
"build-storybook": "storybook build --output-dir storybook-static"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"react",
|
|
39
|
+
"microsoft-graph",
|
|
40
|
+
"fluent-ui",
|
|
41
|
+
"components",
|
|
42
|
+
"graph-toolkit"
|
|
43
|
+
],
|
|
44
|
+
"author": "",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@azure/msal-browser": "^4.0.0 || ^5.0.0",
|
|
51
|
+
"@fluentui/react-components": "^9.0.0",
|
|
52
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
53
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"@azure/msal-browser": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@microsoft/microsoft-graph-client": "^3.0.7",
|
|
62
|
+
"@microsoft/microsoft-graph-types": "^2.40.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@changesets/cli": "^2.29.7",
|
|
66
|
+
"@azure/msal-browser": "^5.2.0",
|
|
67
|
+
"@fluentui/react-components": "^9.72.6",
|
|
68
|
+
"@storybook/addon-a11y": "^10.0.6",
|
|
69
|
+
"@storybook/addon-docs": "^10.0.6",
|
|
70
|
+
"@storybook/addon-links": "^10.0.6",
|
|
71
|
+
"@storybook/react-vite": "^10.0.6",
|
|
72
|
+
"@testing-library/react": "^16.3.0",
|
|
73
|
+
"@types/react": "^18.2.0",
|
|
74
|
+
"@types/react-dom": "^18.2.0",
|
|
75
|
+
"@typescript-eslint/eslint-plugin": "^8.46.3",
|
|
76
|
+
"@typescript-eslint/parser": "^8.46.3",
|
|
77
|
+
"@vitejs/plugin-react": "^5.1.0",
|
|
78
|
+
"eslint": "^9.39.1",
|
|
79
|
+
"eslint-config-prettier": "^10.1.8",
|
|
80
|
+
"eslint-plugin-react": "^7.33.2",
|
|
81
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
82
|
+
"eslint-plugin-storybook": "^10.2.8",
|
|
83
|
+
"globals": "^17.3.0",
|
|
84
|
+
"jsdom": "^27.1.0",
|
|
85
|
+
"prettier": "^3.2.5",
|
|
86
|
+
"react": "^18.2.0",
|
|
87
|
+
"react-dom": "^18.2.0",
|
|
88
|
+
"storybook": "^10.0.6",
|
|
89
|
+
"typescript": "^5.9.3",
|
|
90
|
+
"vite": "^7.2.2",
|
|
91
|
+
"vite-plugin-dts": "^4.5.4",
|
|
92
|
+
"vitest": "^4.0.18"
|
|
93
|
+
}
|
|
94
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
Graph Toolkit React
|
|
3
|
+
</h1>
|
|
4
|
+
|
|
5
|
+
<h4 align="center">Modern React Components for <a href="https://graph.microsoft.com">Microsoft Graph</a> powered by <a href="https://react.fluentui.dev/">Fluent UI</a></h4>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="https://img.shields.io/badge/alpha-0.1.0-orange" alt="Alpha Release">
|
|
9
|
+
<img src="https://img.shields.io/badge/React-18%2F19-blue" alt="React 18/19">
|
|
10
|
+
<img src="https://img.shields.io/badge/Fluent_UI-9.72-purple" alt="Fluent UI 9">
|
|
11
|
+
<img src="https://img.shields.io/badge/TypeScript-5.9-blue" alt="TypeScript 5.9">
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<h3 align="center">
|
|
15
|
+
<a href="#getting-started">Get Started</a> •
|
|
16
|
+
<a href="#storybook">Storybook</a> •
|
|
17
|
+
<a href="#contribute">Contribute</a>
|
|
18
|
+
</h3>
|
|
19
|
+
|
|
20
|
+
## 🎯 Project Vision
|
|
21
|
+
|
|
22
|
+
This project provides a **modern, React-first component library** built on top of Fluent UI to help React developers integrate with Microsoft Graph quickly and consistently.
|
|
23
|
+
|
|
24
|
+
> Note: This project is a spiritual successor to Microsoft Graph Toolkit, redesigned as a React-first library.
|
|
25
|
+
|
|
26
|
+
### Key Principles
|
|
27
|
+
|
|
28
|
+
- **React-First**: Built specifically for React applications, not framework-agnostic web components
|
|
29
|
+
- **Fluent UI Native**: Uses official Fluent UI v9 components instead of custom implementations
|
|
30
|
+
- **Single Package**: One cohesive npm package instead of a complex monorepo
|
|
31
|
+
- **Modern Stack**: TypeScript 5, Vite, Vitest, and Storybook
|
|
32
|
+
- **Developer Experience**: Simple API, excellent TypeScript support, comprehensive documentation
|
|
33
|
+
|
|
34
|
+
### Design Philosophy
|
|
35
|
+
|
|
36
|
+
Instead of maintaining the original web component approach, this fork:
|
|
37
|
+
|
|
38
|
+
1. **Leverages Fluent UI**: Uses the official `Persona` component from Fluent UI instead of custom Avatar/Text components
|
|
39
|
+
2. **Embraces React Patterns**: Components designed for React's patterns (hooks, context) from the ground up
|
|
40
|
+
3. **Reduces Complexity**: Smaller codebase enables quicker development and easier maintenance
|
|
41
|
+
4. **Focuses on Quality**: Fewer components, better implementation, comprehensive documentation
|
|
42
|
+
|
|
43
|
+
## 📦 Package
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install @devsym/graph-toolkit-react
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
| Package | Version | Description |
|
|
50
|
+
| ------- | ------- | ----------- |
|
|
51
|
+
| `@devsym/graph-toolkit-react` | `0.1.0-alpha.1` | React components for Microsoft Graph powered by Fluent UI |
|
|
52
|
+
|
|
53
|
+
## 🎨 Components
|
|
54
|
+
|
|
55
|
+
Currently available in alpha:
|
|
56
|
+
|
|
57
|
+
### Person Component
|
|
58
|
+
A flexible component for displaying user information from Microsoft Graph.
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { Person } from '@devsym/graph-toolkit-react';
|
|
62
|
+
|
|
63
|
+
<Person
|
|
64
|
+
userId="user@contoso.com"
|
|
65
|
+
view="twoLines"
|
|
66
|
+
showPresence
|
|
67
|
+
textAlignment="center"
|
|
68
|
+
onClick={() => console.log('Clicked!')}
|
|
69
|
+
/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
<a id="getting-started"></a>
|
|
74
|
+
## 🚀 Getting Started
|
|
75
|
+
|
|
76
|
+
### Prerequisites
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
81
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
82
|
+
"@fluentui/react-components": "^9.0.0"
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Installation
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm install --save @devsym/graph-toolkit-react
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Also install required peer dependencies in your app if they are not already present:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm install --save react react-dom @fluentui/react-components
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
If you plan to use `MsalBrowserProvider`, also install:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm install --save @azure/msal-browser
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If you only use `TeamsHostedProvider` (Teams-hosted app with consumer-managed Teams auth), `@azure/msal-browser` is not required.
|
|
105
|
+
|
|
106
|
+
### Quick Start
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { FluentProvider, webLightTheme } from '@fluentui/react-components';
|
|
110
|
+
import { GraphProvider, MockProvider, Person } from '@devsym/graph-toolkit-react';
|
|
111
|
+
|
|
112
|
+
function App() {
|
|
113
|
+
const provider = new MockProvider(); // Use MockProvider for development
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<FluentProvider theme={webLightTheme}>
|
|
117
|
+
<GraphProvider provider={provider}>
|
|
118
|
+
<Person userId="AdeleV@contoso.com" view="twoLines" showPresence />
|
|
119
|
+
</GraphProvider>
|
|
120
|
+
</FluentProvider>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Authentication Providers
|
|
126
|
+
|
|
127
|
+
The library uses a provider pattern for authentication:
|
|
128
|
+
|
|
129
|
+
- **MockProvider**: Returns mock data (Adele Vance) without API calls - perfect for development
|
|
130
|
+
- **MsalBrowserProvider**: Native MSAL browser provider included in this package
|
|
131
|
+
- **TeamsHostedProvider**: For Teams-hosted apps using consumer-managed Teams login + backend Graph token exchange
|
|
132
|
+
- **Custom Providers**: Implement `IProvider` interface to integrate with other auth systems
|
|
133
|
+
|
|
134
|
+
#### `MsalBrowserProvider` quick start
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { PublicClientApplication } from '@azure/msal-browser';
|
|
138
|
+
import {
|
|
139
|
+
GraphProvider,
|
|
140
|
+
MsalBrowserProvider,
|
|
141
|
+
Person,
|
|
142
|
+
ProviderState,
|
|
143
|
+
} from '@devsym/graph-toolkit-react';
|
|
144
|
+
|
|
145
|
+
const msal = new PublicClientApplication({
|
|
146
|
+
auth: {
|
|
147
|
+
clientId: 'YOUR_CLIENT_ID',
|
|
148
|
+
authority: 'https://login.microsoftonline.com/common',
|
|
149
|
+
redirectUri: window.location.origin,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const provider = new MsalBrowserProvider(msal, ['User.Read']);
|
|
154
|
+
await provider.initialize();
|
|
155
|
+
|
|
156
|
+
function App() {
|
|
157
|
+
return (
|
|
158
|
+
<GraphProvider provider={provider}>
|
|
159
|
+
<Person userId="me" view="threelines" />
|
|
160
|
+
</GraphProvider>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### `TeamsHostedProvider` quick start (consumer-managed Teams login)
|
|
166
|
+
|
|
167
|
+
Use this when your app is hosted in Microsoft Teams and your app code already handles TeamsJS auth for multiple APIs.
|
|
168
|
+
The provider does not open Teams login popups itself; it uses callbacks you provide.
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
import {
|
|
172
|
+
createBackendTokenExchange,
|
|
173
|
+
GraphProvider,
|
|
174
|
+
Person,
|
|
175
|
+
TeamsHostedProvider,
|
|
176
|
+
} from '@devsym/graph-toolkit-react';
|
|
177
|
+
import { authentication } from '@microsoft/teams-js';
|
|
178
|
+
|
|
179
|
+
const exchangeForGraphToken = createBackendTokenExchange({
|
|
180
|
+
endpoint: '/api/token/exchange',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const provider = new TeamsHostedProvider({
|
|
184
|
+
defaultScopes: ['User.Read'],
|
|
185
|
+
getTeamsSsoToken: async (scopes) => {
|
|
186
|
+
// Consumer app owns Teams auth and can reuse token for other APIs
|
|
187
|
+
return authentication.getAuthToken({ resources: scopes });
|
|
188
|
+
},
|
|
189
|
+
exchangeForGraphToken,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await provider.login();
|
|
193
|
+
|
|
194
|
+
function App() {
|
|
195
|
+
return (
|
|
196
|
+
<GraphProvider provider={provider}>
|
|
197
|
+
<Person userId="me" view="threelines" />
|
|
198
|
+
</GraphProvider>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Use explicit mode selection in your app:
|
|
204
|
+
|
|
205
|
+
- Browser-hosted app: instantiate `MsalBrowserProvider`
|
|
206
|
+
- Teams-hosted app: instantiate `TeamsHostedProvider`
|
|
207
|
+
|
|
208
|
+
#### Scopes by feature
|
|
209
|
+
|
|
210
|
+
| Feature | Minimum delegated scope | Notes |
|
|
211
|
+
| ------- | ------------------------ | ----- |
|
|
212
|
+
| Load current user profile (`userId="me"`) | `User.Read` | Required for basic person data (`displayName`, `mail`, etc.) |
|
|
213
|
+
| Load another user by id/upn (`userId="{id}"`) | `User.ReadBasic.All` | May require admin consent depending on tenant policy |
|
|
214
|
+
| Show presence (`showPresence`) | `Presence.Read` | If not granted, person still renders without presence |
|
|
215
|
+
| Load profile photo (`fetchImage`) | `User.Read` | Falls back to initials if photo is unavailable |
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
import { IProvider, ProviderState } from '@devsym/graph-toolkit-react';
|
|
219
|
+
|
|
220
|
+
class MyAuthProvider implements IProvider {
|
|
221
|
+
state = ProviderState.SignedOut;
|
|
222
|
+
|
|
223
|
+
async login(): Promise<void> { /* ... */ }
|
|
224
|
+
async logout(): Promise<void> { /* ... */ }
|
|
225
|
+
async getAccessToken(): Promise<string> { /* ... */ }
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
<a id="storybook"></a>
|
|
230
|
+
## 📚 Storybook
|
|
231
|
+
|
|
232
|
+
Explore all components interactively in our Storybook documentation:
|
|
233
|
+
|
|
234
|
+
**[View Storybook →](https://thomaspe.github.io/graph-toolkit-react/)** _(Deployed to GitHub Pages on every main branch update)_
|
|
235
|
+
|
|
236
|
+
Run locally:
|
|
237
|
+
```bash
|
|
238
|
+
npm run storybook
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## 🛠️ Development
|
|
242
|
+
|
|
243
|
+
### Tech Stack
|
|
244
|
+
|
|
245
|
+
- **Build**: Vite 7 (ESM + CJS output)
|
|
246
|
+
- **Language**: TypeScript 5.9
|
|
247
|
+
- **Testing**: Vitest 3 + Testing Library
|
|
248
|
+
- **Documentation**: Storybook 10
|
|
249
|
+
- **UI Framework**: Fluent UI v9 (React Components)
|
|
250
|
+
- **Graph SDK**: @microsoft/microsoft-graph-client 3.0
|
|
251
|
+
|
|
252
|
+
### Build & Test
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# Install dependencies
|
|
256
|
+
npm install
|
|
257
|
+
|
|
258
|
+
# Type check
|
|
259
|
+
npm run type-check
|
|
260
|
+
|
|
261
|
+
# Lint
|
|
262
|
+
npm run lint
|
|
263
|
+
|
|
264
|
+
# Build library
|
|
265
|
+
npm run build
|
|
266
|
+
|
|
267
|
+
# Run tests
|
|
268
|
+
npm run test
|
|
269
|
+
|
|
270
|
+
# Run Storybook
|
|
271
|
+
npm run storybook
|
|
272
|
+
|
|
273
|
+
# Build Storybook for deployment
|
|
274
|
+
npm run build-storybook
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Project Structure
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
src/
|
|
281
|
+
├── components/
|
|
282
|
+
│ └── Person/
|
|
283
|
+
│ ├── Person.tsx # Component implementation
|
|
284
|
+
│ ├── Person.types.ts # TypeScript definitions
|
|
285
|
+
│ └── __tests__/ # Component tests
|
|
286
|
+
├── providers/
|
|
287
|
+
│ ├── IProvider.ts # Provider interface
|
|
288
|
+
│ ├── MockProvider.ts # Development provider
|
|
289
|
+
│ └── ProviderContext.tsx # React context
|
|
290
|
+
├── hooks/
|
|
291
|
+
│ ├── useGraphClient.ts # Graph client hook
|
|
292
|
+
│ ├── usePersonData.ts # Person data fetching
|
|
293
|
+
│ └── useProvider.ts # Provider access
|
|
294
|
+
└── index.ts # Public API
|
|
295
|
+
|
|
296
|
+
stories/
|
|
297
|
+
└── Person.stories.tsx # Storybook stories (19 examples)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## 🎯 Current Status
|
|
301
|
+
|
|
302
|
+
**Alpha** ✅
|
|
303
|
+
|
|
304
|
+
- ✅ Repository restructured from monorepo to single package
|
|
305
|
+
- ✅ Provider infrastructure with MockProvider for development
|
|
306
|
+
- ✅ Person component using Fluent UI Persona
|
|
307
|
+
- ✅ Full Persona configuration support (textAlignment, textPosition, sizing)
|
|
308
|
+
- ✅ Build system (ESM + CJS + TypeScript declarations)
|
|
309
|
+
- ✅ Storybook documentation and GitHub Pages deployment
|
|
310
|
+
- ✅ CI/CD with GitHub Actions
|
|
311
|
+
- ✅ Automatic Storybook deployment to GitHub Pages
|
|
312
|
+
- ✅ Sample app with MSAL sign-in and `Person` (`userId="me"`) at `samples/react-msal-sample`
|
|
313
|
+
|
|
314
|
+
**Next Steps**:
|
|
315
|
+
- Additional components (PeoplePicker, PersonCard, Login)
|
|
316
|
+
- Expanded provider guidance and production auth examples
|
|
317
|
+
- Comprehensive test coverage
|
|
318
|
+
- First stable release (1.0.0)
|
|
319
|
+
|
|
320
|
+
<a id="contribute"></a>
|
|
321
|
+
## 🤝 Contribute
|
|
322
|
+
|
|
323
|
+
This **community-driven project** provides React developers with components for Microsoft Graph. Contributions are welcome!
|
|
324
|
+
|
|
325
|
+
### Guidelines
|
|
326
|
+
|
|
327
|
+
1. Follow the existing code style (TypeScript, ESLint, Prettier)
|
|
328
|
+
2. Use Fluent UI components whenever possible
|
|
329
|
+
3. Write tests for new components
|
|
330
|
+
4. Add Storybook stories demonstrating all component features
|
|
331
|
+
5. Update documentation
|
|
332
|
+
|
|
333
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
|
|
334
|
+
|
|
335
|
+
## 📝 License
|
|
336
|
+
|
|
337
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
338
|
+
|
|
339
|
+
## 🙏 Acknowledgments
|
|
340
|
+
|
|
341
|
+
- **Fluent UI Team**: For the excellent React component library
|
|
342
|
+
- **Microsoft Graph**: For the powerful API that makes this all possible
|
|
343
|
+
- **Community Contributors**: For helping improve this project
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
**Note**: This is an alpha release under active development. APIs may change. Not recommended for production use yet.
|
|
348
|
+
|
|
349
|
+
## 🔗 Links
|
|
350
|
+
|
|
351
|
+
- **Fluent UI**: [react.fluentui.dev](https://react.fluentui.dev/)
|
|
352
|
+
- **Microsoft Graph**: [graph.microsoft.com](https://graph.microsoft.com)
|
|
353
|
+
- **Changelog**: [CHANGELOG.md](CHANGELOG.md)
|
|
354
|
+
- **Publishing**: [PUBLISHING.md](PUBLISHING.md)
|
|
355
|
+
|