@powerportalspro/react 6.0.0 → 7.0.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/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ChangeEvent, ReactNode } from 'react';
3
- import { components, TableMetadata as TableMetadata$1, ViewMetadata, CurrentUserInfo, Transport, AuthClient, PowerPortalsProClient, LoginRequest, LoginResponse, SwitchIdentityResponse, PowerPortalsProError, TableRecord, RetrieveRecordsResponse, GridDataRequest, GridDataResponse, GridFilterBase, ChartAggregation, ColumnSort, ResolvedColumn, TableSecurityPermission, ChartData, OrganizationRequest } from '@powerportalspro/core';
3
+ import { components, TableMetadata as TableMetadata$1, ViewMetadata, CurrentUserInfo, Transport, AuthClient, PowerPortalsProClient, LoginRequest, LoginResponse, SwitchIdentityResponse, ImpersonateResponse, StopImpersonationResponse, ImpersonationSearchResponse, PowerPortalsProError, TableRecord, RetrieveRecordsResponse, GridDataRequest, GridDataResponse, GridFilterBase, ChartAggregation, ColumnSort, ResolvedColumn, TableSecurityPermission, ChartData, OrganizationRequest } from '@powerportalspro/core';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
5
 
6
6
  /**
@@ -343,6 +343,29 @@ interface PowerPortalsProContextValue {
343
343
  * alt id is read from the cookie's claims server-side.
344
344
  */
345
345
  readonly switchIdentity: () => Promise<SwitchIdentityResponse>;
346
+ /**
347
+ * Starts impersonating a portal contact, swapping the auth cookie for theirs. Permitted
348
+ * server-side only for internal users holding the `SystemAdmin` role, and only ever resolves a
349
+ * contact — so it can never be used to assume another staff identity.
350
+ *
351
+ * On `ImpersonateResult.Started` the auth state is refreshed from `/api/auth/me` and now
352
+ * describes the CONTACT: `user.userId`, `user.roles` and `user.portalUserType` are theirs, with
353
+ * `user.isImpersonating` and `user.impersonatorName` the only trace of who is behind it. Other
354
+ * outcomes leave the state untouched so the caller can surface a message.
355
+ */
356
+ readonly impersonate: (targetUserId: string) => Promise<ImpersonateResponse>;
357
+ /**
358
+ * Ends an impersonation session and restores the internal user. Takes no parameters — the
359
+ * identity to return to is read from the session's claims server-side. On
360
+ * `StopImpersonationResult.Stopped` the auth state is refreshed.
361
+ */
362
+ readonly stopImpersonation: () => Promise<StopImpersonationResponse>;
363
+ /**
364
+ * Searches contacts the current user may impersonate. Returns an empty result — never an
365
+ * error — when the caller isn't permitted to impersonate or the query is under three
366
+ * characters, so a picker can render "no matches" without branching on why.
367
+ */
368
+ readonly searchImpersonationTargets: (query: string) => Promise<ImpersonationSearchResponse>;
346
369
  }
347
370
  declare const PowerPortalsProContext: react.Context<PowerPortalsProContextValue | null>;
348
371
 
@@ -482,6 +505,27 @@ type UseAuthResult = AuthState & {
482
505
  * On `Switched`, the in-memory auth state is refreshed automatically.
483
506
  */
484
507
  switchIdentity: () => Promise<SwitchIdentityResponse>;
508
+ /**
509
+ * Starts impersonating a portal contact. Allowed server-side only for internal users holding
510
+ * the `SystemAdmin` role, and only ever resolves a contact — never another staff account.
511
+ *
512
+ * On `ImpersonateResult.Started` the auth state refreshes and `auth.user` becomes the CONTACT:
513
+ * their id, their roles, their user type. `auth.user.isImpersonating` and
514
+ * `auth.user.impersonatorName` are the only fields that say otherwise, which is what makes the
515
+ * impersonated session an honest rehearsal of what that contact actually sees.
516
+ */
517
+ impersonate: (targetUserId: string) => Promise<ImpersonateResponse>;
518
+ /**
519
+ * Ends an impersonation session and restores the internal user. Takes no parameters — the
520
+ * identity to return to comes from the session's own claims, so the client can't pick it.
521
+ */
522
+ stopImpersonation: () => Promise<StopImpersonationResponse>;
523
+ /**
524
+ * Searches contacts the current user may impersonate — by name, email, or exact user id.
525
+ * Returns an empty result rather than an error when the caller isn't permitted or the query is
526
+ * too short, so a picker renders "no matches" without branching on why.
527
+ */
528
+ searchImpersonationTargets: (query: string) => Promise<ImpersonationSearchResponse>;
485
529
  };
486
530
  declare function useAuth(): UseAuthResult;
487
531
 
@@ -800,6 +844,16 @@ interface ViewDataSource {
800
844
  readonly status: QueryStatus;
801
845
  readonly rows: readonly TableRecord[];
802
846
  readonly totalCount: number | string | null | undefined;
847
+ /**
848
+ * Dataverse's `MoreRecords` flag from the most recent response — the
849
+ * authoritative "is there another page?" signal for sequential paging.
850
+ * Prefer it over comparing rows against {@link totalCount}: the count is
851
+ * computed before the server's per-row permission handlers drop rows,
852
+ * so the rows a consumer accumulates can legitimately fall short of it.
853
+ * `undefined` until the first response lands; `null` when the server
854
+ * didn't report one.
855
+ */
856
+ readonly moreRecords: boolean | null | undefined;
803
857
  readonly columns: readonly ResolvedColumn[];
804
858
  /**
805
859
  * Current user's combined table-level `TableSecurityPermission` mask for
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ChangeEvent, ReactNode } from 'react';
3
- import { components, TableMetadata as TableMetadata$1, ViewMetadata, CurrentUserInfo, Transport, AuthClient, PowerPortalsProClient, LoginRequest, LoginResponse, SwitchIdentityResponse, PowerPortalsProError, TableRecord, RetrieveRecordsResponse, GridDataRequest, GridDataResponse, GridFilterBase, ChartAggregation, ColumnSort, ResolvedColumn, TableSecurityPermission, ChartData, OrganizationRequest } from '@powerportalspro/core';
3
+ import { components, TableMetadata as TableMetadata$1, ViewMetadata, CurrentUserInfo, Transport, AuthClient, PowerPortalsProClient, LoginRequest, LoginResponse, SwitchIdentityResponse, ImpersonateResponse, StopImpersonationResponse, ImpersonationSearchResponse, PowerPortalsProError, TableRecord, RetrieveRecordsResponse, GridDataRequest, GridDataResponse, GridFilterBase, ChartAggregation, ColumnSort, ResolvedColumn, TableSecurityPermission, ChartData, OrganizationRequest } from '@powerportalspro/core';
4
4
  import * as react_jsx_runtime from 'react/jsx-runtime';
5
5
 
6
6
  /**
@@ -343,6 +343,29 @@ interface PowerPortalsProContextValue {
343
343
  * alt id is read from the cookie's claims server-side.
344
344
  */
345
345
  readonly switchIdentity: () => Promise<SwitchIdentityResponse>;
346
+ /**
347
+ * Starts impersonating a portal contact, swapping the auth cookie for theirs. Permitted
348
+ * server-side only for internal users holding the `SystemAdmin` role, and only ever resolves a
349
+ * contact — so it can never be used to assume another staff identity.
350
+ *
351
+ * On `ImpersonateResult.Started` the auth state is refreshed from `/api/auth/me` and now
352
+ * describes the CONTACT: `user.userId`, `user.roles` and `user.portalUserType` are theirs, with
353
+ * `user.isImpersonating` and `user.impersonatorName` the only trace of who is behind it. Other
354
+ * outcomes leave the state untouched so the caller can surface a message.
355
+ */
356
+ readonly impersonate: (targetUserId: string) => Promise<ImpersonateResponse>;
357
+ /**
358
+ * Ends an impersonation session and restores the internal user. Takes no parameters — the
359
+ * identity to return to is read from the session's claims server-side. On
360
+ * `StopImpersonationResult.Stopped` the auth state is refreshed.
361
+ */
362
+ readonly stopImpersonation: () => Promise<StopImpersonationResponse>;
363
+ /**
364
+ * Searches contacts the current user may impersonate. Returns an empty result — never an
365
+ * error — when the caller isn't permitted to impersonate or the query is under three
366
+ * characters, so a picker can render "no matches" without branching on why.
367
+ */
368
+ readonly searchImpersonationTargets: (query: string) => Promise<ImpersonationSearchResponse>;
346
369
  }
347
370
  declare const PowerPortalsProContext: react.Context<PowerPortalsProContextValue | null>;
348
371
 
@@ -482,6 +505,27 @@ type UseAuthResult = AuthState & {
482
505
  * On `Switched`, the in-memory auth state is refreshed automatically.
483
506
  */
484
507
  switchIdentity: () => Promise<SwitchIdentityResponse>;
508
+ /**
509
+ * Starts impersonating a portal contact. Allowed server-side only for internal users holding
510
+ * the `SystemAdmin` role, and only ever resolves a contact — never another staff account.
511
+ *
512
+ * On `ImpersonateResult.Started` the auth state refreshes and `auth.user` becomes the CONTACT:
513
+ * their id, their roles, their user type. `auth.user.isImpersonating` and
514
+ * `auth.user.impersonatorName` are the only fields that say otherwise, which is what makes the
515
+ * impersonated session an honest rehearsal of what that contact actually sees.
516
+ */
517
+ impersonate: (targetUserId: string) => Promise<ImpersonateResponse>;
518
+ /**
519
+ * Ends an impersonation session and restores the internal user. Takes no parameters — the
520
+ * identity to return to comes from the session's own claims, so the client can't pick it.
521
+ */
522
+ stopImpersonation: () => Promise<StopImpersonationResponse>;
523
+ /**
524
+ * Searches contacts the current user may impersonate — by name, email, or exact user id.
525
+ * Returns an empty result rather than an error when the caller isn't permitted or the query is
526
+ * too short, so a picker renders "no matches" without branching on why.
527
+ */
528
+ searchImpersonationTargets: (query: string) => Promise<ImpersonationSearchResponse>;
485
529
  };
486
530
  declare function useAuth(): UseAuthResult;
487
531
 
@@ -800,6 +844,16 @@ interface ViewDataSource {
800
844
  readonly status: QueryStatus;
801
845
  readonly rows: readonly TableRecord[];
802
846
  readonly totalCount: number | string | null | undefined;
847
+ /**
848
+ * Dataverse's `MoreRecords` flag from the most recent response — the
849
+ * authoritative "is there another page?" signal for sequential paging.
850
+ * Prefer it over comparing rows against {@link totalCount}: the count is
851
+ * computed before the server's per-row permission handlers drop rows,
852
+ * so the rows a consumer accumulates can legitimately fall short of it.
853
+ * `undefined` until the first response lands; `null` when the server
854
+ * didn't report one.
855
+ */
856
+ readonly moreRecords: boolean | null | undefined;
803
857
  readonly columns: readonly ResolvedColumn[];
804
858
  /**
805
859
  * Current user's combined table-level `TableSecurityPermission` mask for
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createContext, useRef, useState, useEffect, useLayoutEffect, useCallback, useContext, useMemo, useSyncExternalStore, useReducer, useId } from 'react';
2
- import { Transport, AuthClient, PowerPortalsProClient, LoginResult, SwitchIdentityResult } from '@powerportalspro/core';
2
+ import { Transport, AuthClient, PowerPortalsProClient, LoginResult, SwitchIdentityResult, ImpersonateResult, StopImpersonationResult } from '@powerportalspro/core';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
 
5
5
  // src/masking/reformat.ts
@@ -1057,6 +1057,24 @@ function PowerPortalsProProvider({
1057
1057
  }
1058
1058
  return response;
1059
1059
  }, [auth, refreshAuth]);
1060
+ const impersonate = useCallback(async (targetUserId) => {
1061
+ const response = await auth.impersonateAsync(targetUserId);
1062
+ if (response.result === ImpersonateResult.Started) {
1063
+ await refreshAuth();
1064
+ }
1065
+ return response;
1066
+ }, [auth, refreshAuth]);
1067
+ const stopImpersonation = useCallback(async () => {
1068
+ const response = await auth.stopImpersonationAsync();
1069
+ if (response.result === StopImpersonationResult.Stopped || response.result === StopImpersonationResult.ImpersonatorNotFound) {
1070
+ await refreshAuth();
1071
+ }
1072
+ return response;
1073
+ }, [auth, refreshAuth]);
1074
+ const searchImpersonationTargets = useCallback(
1075
+ (query) => auth.searchImpersonationTargetsAsync(query),
1076
+ [auth]
1077
+ );
1060
1078
  useEffect(() => {
1061
1079
  refreshAuth();
1062
1080
  }, [refreshAuth]);
@@ -1073,7 +1091,10 @@ function PowerPortalsProProvider({
1073
1091
  refreshAuth,
1074
1092
  login,
1075
1093
  logout,
1076
- switchIdentity
1094
+ switchIdentity,
1095
+ impersonate,
1096
+ stopImpersonation,
1097
+ searchImpersonationTargets
1077
1098
  }),
1078
1099
  [
1079
1100
  transportInstance,
@@ -1087,7 +1108,10 @@ function PowerPortalsProProvider({
1087
1108
  refreshAuth,
1088
1109
  login,
1089
1110
  logout,
1090
- switchIdentity
1111
+ switchIdentity,
1112
+ impersonate,
1113
+ stopImpersonation,
1114
+ searchImpersonationTargets
1091
1115
  ]
1092
1116
  );
1093
1117
  let localizationContent;
@@ -1141,13 +1165,25 @@ function CacheLocaleSync() {
1141
1165
 
1142
1166
  // src/use-auth.ts
1143
1167
  function useAuth() {
1144
- const { authState, refreshAuth, login, logout, switchIdentity } = usePowerPortalsPro();
1168
+ const {
1169
+ authState,
1170
+ refreshAuth,
1171
+ login,
1172
+ logout,
1173
+ switchIdentity,
1174
+ impersonate,
1175
+ stopImpersonation,
1176
+ searchImpersonationTargets
1177
+ } = usePowerPortalsPro();
1145
1178
  return {
1146
1179
  ...authState,
1147
1180
  refresh: refreshAuth,
1148
1181
  login,
1149
1182
  logout,
1150
- switchIdentity
1183
+ switchIdentity,
1184
+ impersonate,
1185
+ stopImpersonation,
1186
+ searchImpersonationTargets
1151
1187
  };
1152
1188
  }
1153
1189
  function AuthorizedView({ children, roles, fallback = null }) {
@@ -1401,6 +1437,7 @@ function useViewDataSource(options) {
1401
1437
  const rows = data?.tableRecords ?? EMPTY_RECORDS;
1402
1438
  const columns = data?.columns ?? EMPTY_COLUMNS;
1403
1439
  const totalCount = data?.pagingInfo?.totalRecordCount;
1440
+ const moreRecords = data?.pagingInfo?.moreRecords;
1404
1441
  const tablePermissions = data?.tablePermissions;
1405
1442
  const aggregations = options.aggregations ?? EMPTY_AGGREGATION_SPEC;
1406
1443
  const isFetchingAggregations = query.isFetching && debouncedRequest.aggregations !== void 0 && debouncedRequest.aggregations !== null && debouncedRequest.aggregations.length > 0;
@@ -1426,6 +1463,7 @@ function useViewDataSource(options) {
1426
1463
  status: query.status,
1427
1464
  rows,
1428
1465
  totalCount,
1466
+ moreRecords,
1429
1467
  columns,
1430
1468
  tablePermissions,
1431
1469
  aggregationResults,
@@ -1451,6 +1489,7 @@ function useViewDataSource(options) {
1451
1489
  query.status,
1452
1490
  rows,
1453
1491
  totalCount,
1492
+ moreRecords,
1454
1493
  columns,
1455
1494
  tablePermissions,
1456
1495
  aggregationResults,