@powerportalspro/react-fluent 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.cjs CHANGED
@@ -304,8 +304,12 @@ function isRecordWriteDenied(record) {
304
304
  const perms = record.permissions ?? 0;
305
305
  return (perms & core.TableSecurityPermission.Write) === 0;
306
306
  }
307
- function resolveEditState(metadata, record, explicitReadOnly, explicitDisabled) {
308
- if (isRecordWriteDenied(record)) {
307
+ function isColumnWriteDenied(record, columnName) {
308
+ if (!record || !columnName) return false;
309
+ return !core.canWriteColumn(record, columnName);
310
+ }
311
+ function resolveEditState(metadata, record, explicitReadOnly, explicitDisabled, columnName) {
312
+ if (isRecordWriteDenied(record) || isColumnWriteDenied(record, columnName)) {
309
313
  return { readOnly: true, disabled: true };
310
314
  }
311
315
  return {
@@ -733,7 +737,8 @@ function TextEdit({
733
737
  metadata,
734
738
  record,
735
739
  readOnly,
736
- disabled
740
+ disabled,
741
+ columnName
737
742
  );
738
743
  const setValue = useGuardedSetValue(rawSetValue, resolvedReadOnly, resolvedDisabled);
739
744
  const validationErrors = useColumnValidation(
@@ -827,7 +832,8 @@ function MemoEdit({
827
832
  metadata,
828
833
  record,
829
834
  readOnly,
830
- disabled
835
+ disabled,
836
+ columnName
831
837
  );
832
838
  const setValue = useGuardedSetValue(rawSetValue, resolvedReadOnly, resolvedDisabled);
833
839
  const validationErrors = useColumnValidation(
@@ -941,7 +947,8 @@ function BoolEdit({
941
947
  metadata,
942
948
  record,
943
949
  readOnly,
944
- disabled
950
+ disabled,
951
+ columnName
945
952
  );
946
953
  const setValue = useGuardedSetValue(rawSetValue, resolvedReadOnly, resolvedDisabled);
947
954
  const isInactive = resolvedDisabled || resolvedReadOnly;
@@ -1117,7 +1124,8 @@ function NumberEdit({
1117
1124
  metadata,
1118
1125
  record,
1119
1126
  readOnly,
1120
- disabled
1127
+ disabled,
1128
+ columnName
1121
1129
  );
1122
1130
  const setValue = useGuardedSetValue(rawSetValue, resolvedReadOnly, resolvedDisabled);
1123
1131
  const validationErrors = useColumnValidation(
@@ -1561,7 +1569,8 @@ function MoneyEdit({
1561
1569
  metadata,
1562
1570
  record,
1563
1571
  readOnly,
1564
- disabled
1572
+ disabled,
1573
+ columnName
1565
1574
  );
1566
1575
  const setValue = useGuardedSetValue(rawSetValue, resolvedReadOnly, resolvedDisabled);
1567
1576
  const validationErrors = useColumnValidation(
@@ -1975,7 +1984,8 @@ function DateTimeEdit({
1975
1984
  metadata,
1976
1985
  record,
1977
1986
  readOnly,
1978
- disabled
1987
+ disabled,
1988
+ columnName
1979
1989
  );
1980
1990
  const setValue = useGuardedSetValue(rawSetValue, resolvedReadOnly, resolvedDisabled);
1981
1991
  const resolvedEditorType = editorType ?? resolveEditorTypeFromMetadata(metadata);
@@ -5366,7 +5376,8 @@ function FileEdit({
5366
5376
  metadata,
5367
5377
  record,
5368
5378
  readOnly,
5369
- disabled
5379
+ disabled,
5380
+ columnName
5370
5381
  );
5371
5382
  const setValue = useGuardedSetValue(rawSetValue, resolvedReadOnly, resolvedDisabled);
5372
5383
  const isInteractable = !resolvedReadOnly && !resolvedDisabled;
@@ -5683,7 +5694,8 @@ function ImageEdit({
5683
5694
  metadata,
5684
5695
  record,
5685
5696
  readOnly,
5686
- disabled
5697
+ disabled,
5698
+ columnName
5687
5699
  );
5688
5700
  const setValue = useGuardedSetValue(rawSetValue, resolvedReadOnly, resolvedDisabled);
5689
5701
  const isInteractable = !resolvedReadOnly && !resolvedDisabled;
@@ -6163,7 +6175,8 @@ function ChoiceEdit({
6163
6175
  metadata,
6164
6176
  record,
6165
6177
  readOnly,
6166
- disabled
6178
+ disabled,
6179
+ columnName
6167
6180
  );
6168
6181
  const setValue = useGuardedSetValue(rawSetValue, resolvedReadOnly, resolvedDisabled);
6169
6182
  const choiceMeta = getChoiceMetadata(metadata);
@@ -6383,7 +6396,8 @@ function MultiSelectChoiceEdit({
6383
6396
  metadata,
6384
6397
  record,
6385
6398
  readOnly,
6386
- disabled
6399
+ disabled,
6400
+ columnName
6387
6401
  );
6388
6402
  const setValue = useGuardedSetValue(rawSetValue, resolvedReadOnly, resolvedDisabled);
6389
6403
  const choiceMeta = getChoiceMetadata(metadata);
@@ -6505,30 +6519,30 @@ function foldString(s, caseSensitive) {
6505
6519
  function tokenizeSearch(search, delimiters) {
6506
6520
  if (!delimiters) return [search];
6507
6521
  const delim = new Set(Array.from(delimiters));
6508
- const tokens43 = [];
6522
+ const tokens44 = [];
6509
6523
  let current = "";
6510
6524
  for (const ch of search) {
6511
6525
  if (delim.has(ch)) {
6512
6526
  if (current.length > 0) {
6513
- tokens43.push(current);
6527
+ tokens44.push(current);
6514
6528
  current = "";
6515
6529
  }
6516
6530
  } else {
6517
6531
  current += ch;
6518
6532
  }
6519
6533
  }
6520
- if (current.length > 0) tokens43.push(current);
6521
- return tokens43;
6534
+ if (current.length > 0) tokens44.push(current);
6535
+ return tokens44;
6522
6536
  }
6523
6537
  function splitHighlightSegments(text, search, options) {
6524
6538
  if (!text || !search) return [];
6525
6539
  const caseSensitive = options?.caseSensitive ?? false;
6526
6540
  const delimiters = options?.delimiters ?? "";
6527
- const tokens43 = tokenizeSearch(search, delimiters);
6528
- if (tokens43.length === 0) return [{ text, isMatch: false }];
6541
+ const tokens44 = tokenizeSearch(search, delimiters);
6542
+ if (tokens44.length === 0) return [{ text, isMatch: false }];
6529
6543
  const folded = foldString(text, caseSensitive);
6530
6544
  const ranges = [];
6531
- for (const token of tokens43) {
6545
+ for (const token of tokens44) {
6532
6546
  const tokenFolded = foldString(token, caseSensitive).folded;
6533
6547
  if (tokenFolded.length === 0) continue;
6534
6548
  let cursor2 = 0;
@@ -7299,7 +7313,11 @@ function getRendererTypeForCode(typeCode) {
7299
7313
  }
7300
7314
  function ColumnEdit(props) {
7301
7315
  const metadata = react$1.useColumnMetadata(props.columnName);
7316
+ const record = react$1.useRecordContextOptional()?.record;
7302
7317
  const typeCode = metadata?.$type;
7318
+ if (record && !core.canReadColumn(record, props.columnName)) {
7319
+ return /* @__PURE__ */ jsxRuntime.jsx(ColumnDisplayValue, { ...props });
7320
+ }
7303
7321
  switch (typeCode) {
7304
7322
  case 0:
7305
7323
  return /* @__PURE__ */ jsxRuntime.jsx(BoolEdit, { ...props });
@@ -8319,6 +8337,36 @@ function MainGridVirtualizeFooter({
8319
8337
  ] });
8320
8338
  }
8321
8339
 
8340
+ // src/grids/virtualize-paging.ts
8341
+ function hasMoreRowsToLoad(state) {
8342
+ if (state.moreRecords === false) return false;
8343
+ if (state.moreRecords === true) return true;
8344
+ if (state.lastPageEmpty) return false;
8345
+ if (state.totalCount === void 0) return true;
8346
+ return state.accumulatedCount < state.totalCount;
8347
+ }
8348
+ var VirtualizePagingCookies = class {
8349
+ key;
8350
+ byPage = /* @__PURE__ */ new Map();
8351
+ /** Store the cookie the response for `page` came back with (no-op for a null / empty cookie). */
8352
+ record(key, page, cookie) {
8353
+ if (this.key !== key) {
8354
+ this.key = key;
8355
+ this.byPage = /* @__PURE__ */ new Map();
8356
+ }
8357
+ if (cookie) this.byPage.set(page, cookie);
8358
+ }
8359
+ /**
8360
+ * The cookie to send when requesting `page` under `key`: the one page
8361
+ * `page - 1` returned, or `undefined` when it isn't known (page 1, a
8362
+ * non-sequential jump, or a key the store has no cookies for).
8363
+ */
8364
+ cookieFor(key, page) {
8365
+ if (this.key !== key || page <= 1) return void 0;
8366
+ return this.byPage.get(page - 1);
8367
+ }
8368
+ };
8369
+
8322
8370
  // src/grids/persisted-grid-state.ts
8323
8371
  function readPersistedGridStateFromUrl(queryParameterName) {
8324
8372
  if (!queryParameterName) return null;
@@ -8382,6 +8430,12 @@ var ViewSort = {
8382
8430
  /** Localized display name, descending. */
8383
8431
  NameDescending: "nameDescending"
8384
8432
  };
8433
+ function resolveViewLabel(view, t) {
8434
+ const key = `tables.${view.tableName}.views.${view.id}.label`;
8435
+ const localized = t(key);
8436
+ if (localized !== key) return localized;
8437
+ return view.displayName?.trim() ? view.displayName : null;
8438
+ }
8385
8439
  function customToViewMetadata(custom) {
8386
8440
  return {
8387
8441
  id: custom.id,
@@ -8850,8 +8904,22 @@ function MainGridImpl({
8850
8904
  `[MainGrid] Declared <GridColumn> children override the column projection of the supplied fetchXml (columns: ${columnsKey.split(",").join(", ")}). Drop the children \u2014 or remove the conflicting <attribute> elements from the fetchXml \u2014 to silence this message.`
8851
8905
  );
8852
8906
  }, [columnsKey, useInlineFetchXml]);
8907
+ const accumulatorResetKey = react.useMemo(
8908
+ () => [
8909
+ resolvedViewId ?? "",
8910
+ useInlineFetchXml ? inlineFetchXml ?? "" : "",
8911
+ debouncedSearchText,
8912
+ sort.map((s) => `${s.columnName}:${s.descending ? "d" : "a"}`).join("|"),
8913
+ filtersKey,
8914
+ pageSize,
8915
+ tableName ?? ""
8916
+ ].join("|"),
8917
+ [resolvedViewId, useInlineFetchXml, inlineFetchXml, debouncedSearchText, sort, filtersKey, pageSize, tableName]
8918
+ );
8919
+ const [virtualizeCookies] = react.useState(() => new VirtualizePagingCookies());
8853
8920
  const searchRequest = react.useMemo(() => {
8854
8921
  const trimmed = debouncedSearchText.trim();
8922
+ const pagingCookie = isVirtualize ? virtualizeCookies.cookieFor(accumulatorResetKey, page) : void 0;
8855
8923
  return {
8856
8924
  ...useInlineFetchXml && inlineFetchXml ? { fetchXml: inlineFetchXml } : resolvedViewId !== void 0 && { viewId: resolvedViewId },
8857
8925
  ...trimmed && { searchText: trimmed },
@@ -8871,7 +8939,8 @@ function MainGridImpl({
8871
8939
  },
8872
8940
  ...columnsOverride && { columns: [...columnsOverride] },
8873
8941
  pageNumber: page,
8874
- pageSize
8942
+ pageSize,
8943
+ ...pagingCookie && { pagingCookie }
8875
8944
  };
8876
8945
  }, [
8877
8946
  resolvedViewId,
@@ -8882,7 +8951,9 @@ function MainGridImpl({
8882
8951
  page,
8883
8952
  pageSize,
8884
8953
  filtersKey,
8885
- columnsKey
8954
+ columnsKey,
8955
+ isVirtualize,
8956
+ accumulatorResetKey
8886
8957
  ]);
8887
8958
  const transformPending = !!transformViewAsync || isCustomActiveView;
8888
8959
  const recordsResult = react$1.useGridData(searchRequest, {
@@ -9041,7 +9112,7 @@ function MainGridImpl({
9041
9112
  const rowRecord = cellCtx.row.original;
9042
9113
  const rowId = rowRecord.id;
9043
9114
  const isPendingCreateRow = !!rowId && rowId.startsWith("__pending-create-");
9044
- if (editable && !isPendingCreateRow && hasEditorForType) {
9115
+ if (editable && !isPendingCreateRow && hasEditorForType && core.canWriteColumn(rowRecord, columnName)) {
9045
9116
  return (
9046
9117
  // Sizing wrapper — overrides Fluent v9's default
9047
9118
  // <Input> / <Combobox> / <Dropdown> min-width
@@ -9179,18 +9250,6 @@ function MainGridImpl({
9179
9250
  ]);
9180
9251
  const pageRecords = dataSource ? dataSource.rows : recordsResult.data?.tableRecords ?? EMPTY_RECORDS;
9181
9252
  const [accumulatedRows, setAccumulatedRows] = react.useState(null);
9182
- const accumulatorResetKey = react.useMemo(
9183
- () => [
9184
- resolvedViewId ?? "",
9185
- useInlineFetchXml ? inlineFetchXml ?? "" : "",
9186
- debouncedSearchText,
9187
- sort.map((s) => `${s.columnName}:${s.descending ? "d" : "a"}`).join("|"),
9188
- filtersKey,
9189
- pageSize,
9190
- tableName ?? ""
9191
- ].join("|"),
9192
- [resolvedViewId, useInlineFetchXml, inlineFetchXml, debouncedSearchText, sort, filtersKey, pageSize, tableName]
9193
- );
9194
9253
  const resetVirtualizedView = react.useCallback(() => {
9195
9254
  setAccumulatedRows([]);
9196
9255
  setPage(1);
@@ -9216,6 +9275,12 @@ function MainGridImpl({
9216
9275
  return [...base, ...additions];
9217
9276
  });
9218
9277
  }, [isVirtualize, recordsResult.status, pageRecords]);
9278
+ react.useEffect(() => {
9279
+ if (!isVirtualize) return;
9280
+ const landed = recordsResult.data;
9281
+ if (!landed) return;
9282
+ virtualizeCookies.record(accumulatorResetKey, page, landed.pagingInfo?.pagingCookie);
9283
+ }, [isVirtualize, recordsResult.data]);
9219
9284
  const data = isVirtualize ? accumulatedRows ?? EMPTY_RECORDS : pageRecords;
9220
9285
  const [pendingRowUpdates, setPendingRowUpdates] = react.useState(() => /* @__PURE__ */ new Map());
9221
9286
  const [pendingRowCreates, setPendingRowCreates] = react.useState(() => []);
@@ -9696,7 +9761,15 @@ function MainGridImpl({
9696
9761
  if (raw == null) return void 0;
9697
9762
  return typeof raw === "number" ? raw : Number(raw);
9698
9763
  })();
9699
- const hasMoreRowsToLoad = isVirtualize && (totalCount === void 0 || (accumulatedRows?.length ?? 0) < totalCount);
9764
+ const hasMoreRowsToLoad2 = isVirtualize && hasMoreRowsToLoad({
9765
+ accumulatedCount: accumulatedRows?.length ?? 0,
9766
+ totalCount,
9767
+ moreRecords: dataSource ? dataSource.moreRecords : recordsResult.data?.pagingInfo?.moreRecords,
9768
+ // "A page has landed and it carried no rows." In standalone mode
9769
+ // `data` is only ever defined for a landed response (errors wipe it);
9770
+ // the datasource has no data handle, so idle-after-success stands in.
9771
+ lastPageEmpty: dataSource ? dataSource.status === react$1.QueryStatus.Success && !dataSource.isFetching && dataSource.rows.length === 0 : recordsResult.data !== void 0 && pageRecords.length === 0
9772
+ });
9700
9773
  react.useEffect(() => {
9701
9774
  if (!isVirtualize) return;
9702
9775
  const sentinel = loadMoreSentinelRef.current;
@@ -9707,7 +9780,7 @@ function MainGridImpl({
9707
9780
  const entry = entries[0];
9708
9781
  if (!entry?.isIntersecting) return;
9709
9782
  if (recordsResult.isFetching) return;
9710
- if (!hasMoreRowsToLoad) return;
9783
+ if (!hasMoreRowsToLoad2) return;
9711
9784
  setPage(page + 1);
9712
9785
  },
9713
9786
  {
@@ -9720,7 +9793,7 @@ function MainGridImpl({
9720
9793
  );
9721
9794
  observer.observe(sentinel);
9722
9795
  return () => observer.disconnect();
9723
- }, [isVirtualize, hasMoreRowsToLoad, recordsResult.isFetching, page, setPage]);
9796
+ }, [isVirtualize, hasMoreRowsToLoad2, recordsResult.isFetching, page, setPage]);
9724
9797
  const handlePageChange = (nextPage) => {
9725
9798
  if (nextPage === page) return;
9726
9799
  setPage(nextPage);
@@ -9729,8 +9802,8 @@ function MainGridImpl({
9729
9802
  };
9730
9803
  const selectedView = views?.find((v) => v.id === resolvedViewId);
9731
9804
  const selectedViewLabelKey = selectedView ? `tables.${selectedView.tableName}.views.${selectedView.id}.label` : "";
9732
- const selectedViewLabel = selectedView ? t(selectedViewLabelKey) : "";
9733
- const singleViewTitle = selectedView && selectedViewLabel !== selectedViewLabelKey ? selectedViewLabel : selectedView?.displayName ?? void 0;
9805
+ const selectedViewLabel = selectedView ? resolveViewLabel(selectedView, t) ?? selectedViewLabelKey : "";
9806
+ const singleViewTitle = selectedView ? resolveViewLabel(selectedView, t) ?? void 0 : void 0;
9734
9807
  return /* @__PURE__ */ jsxRuntime.jsx(GridContextProvider, { value: gridContextValue, children: /* @__PURE__ */ jsxRuntime.jsxs(
9735
9808
  "div",
9736
9809
  {
@@ -9763,7 +9836,7 @@ function MainGridImpl({
9763
9836
  if (data2.optionValue) handleViewChange(data2.optionValue);
9764
9837
  },
9765
9838
  children: views?.map((view) => {
9766
- const label = t(`tables.${view.tableName}.views.${view.id}.label`);
9839
+ const label = resolveViewLabel(view, t) ?? `tables.${view.tableName}.views.${view.id}.label`;
9767
9840
  return /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Option, { value: view.id, text: label, children: label }, view.id);
9768
9841
  })
9769
9842
  }
@@ -10340,7 +10413,7 @@ function MainGridImpl({
10340
10413
  }
10341
10414
  ),
10342
10415
  displayedData.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.emptyStateRow, children: t("app.components.grid.empty-content") }),
10343
- isVirtualize && hasMoreRowsToLoad && // No spinner here: the full-body `refetchScrim` overlay below is the
10416
+ isVirtualize && hasMoreRowsToLoad2 && // No spinner here: the full-body `refetchScrim` overlay below is the
10344
10417
  // single load indicator. Rendering a `size="tiny"` spinner in the
10345
10418
  // sentinel as well meant a deps-change refetch (refresh / sort /
10346
10419
  // search / view switch) — which resets the accumulator to empty and
@@ -10859,7 +10932,8 @@ function LookupEdit({
10859
10932
  metadata,
10860
10933
  record,
10861
10934
  readOnly,
10862
- disabled
10935
+ disabled,
10936
+ columnName
10863
10937
  );
10864
10938
  const setLookupValue = useGuardedSetValue(
10865
10939
  rawSetLookupValue,
@@ -12639,6 +12713,7 @@ function PowerPortalsProThemeProvider({
12639
12713
  defaultMode = ThemeMode.System,
12640
12714
  defaultAccentColor = "default",
12641
12715
  defaultTextDirection = TextDirection.Ltr,
12716
+ lockAccentColor = false,
12642
12717
  customAccents,
12643
12718
  storageKey = DEFAULT_MODE_STORAGE_KEY,
12644
12719
  style,
@@ -12662,9 +12737,11 @@ function PowerPortalsProThemeProvider({
12662
12737
  () => readStoredMode(storageKey) ?? defaultMode
12663
12738
  );
12664
12739
  const [accentColor, setAccentColorState] = react.useState(() => {
12740
+ const configured = defaultAccentColor in accentMap ? defaultAccentColor : "default";
12741
+ if (lockAccentColor) return configured;
12665
12742
  const stored = readStoredString(accentStorageKey);
12666
12743
  if (stored && stored in accentMap) return stored;
12667
- return defaultAccentColor in accentMap ? defaultAccentColor : "default";
12744
+ return configured;
12668
12745
  });
12669
12746
  const [textDirection, setTextDirectionState] = react.useState(
12670
12747
  () => readStoredDirection(directionStorageKey) ?? defaultTextDirection
@@ -12694,6 +12771,12 @@ function PowerPortalsProThemeProvider({
12694
12771
  document.body.classList.toggle("ppp-theme-dark", isDark);
12695
12772
  document.body.classList.toggle("ppp-theme-light", !isDark);
12696
12773
  }, [isDark]);
12774
+ react.useEffect(() => {
12775
+ if (!lockAccentColor) return;
12776
+ const configured = defaultAccentColor in accentMap ? defaultAccentColor : "default";
12777
+ setAccentColorState(configured);
12778
+ writeStored(accentStorageKey, configured);
12779
+ }, [lockAccentColor, defaultAccentColor, accentMap, accentStorageKey]);
12697
12780
  const setMode = react.useCallback(
12698
12781
  (next) => {
12699
12782
  setModeState(next);
@@ -12703,11 +12786,12 @@ function PowerPortalsProThemeProvider({
12703
12786
  );
12704
12787
  const setAccentColor = react.useCallback(
12705
12788
  (next) => {
12789
+ if (lockAccentColor) return;
12706
12790
  if (!(next in accentMap)) return;
12707
12791
  setAccentColorState(next);
12708
12792
  writeStored(accentStorageKey, next);
12709
12793
  },
12710
- [accentMap, accentStorageKey]
12794
+ [lockAccentColor, accentMap, accentStorageKey]
12711
12795
  );
12712
12796
  const setTextDirection = react.useCallback(
12713
12797
  (next) => {
@@ -12732,6 +12816,7 @@ function PowerPortalsProThemeProvider({
12732
12816
  setAccentColor,
12733
12817
  availableAccents,
12734
12818
  accentColors,
12819
+ accentColorLocked: lockAccentColor,
12735
12820
  textDirection,
12736
12821
  setTextDirection
12737
12822
  }),
@@ -12743,6 +12828,7 @@ function PowerPortalsProThemeProvider({
12743
12828
  setAccentColor,
12744
12829
  availableAccents,
12745
12830
  accentColors,
12831
+ lockAccentColor,
12746
12832
  textDirection,
12747
12833
  setTextDirection
12748
12834
  ]
@@ -12823,7 +12909,7 @@ function ThemeColorSelector({
12823
12909
  width,
12824
12910
  accentLabels
12825
12911
  }) {
12826
- const { accentColor, setAccentColor, availableAccents, accentColors } = useTheme();
12912
+ const { accentColor, setAccentColor, availableAccents, accentColors, accentColorLocked } = useTheme();
12827
12913
  const t = react$1.useT();
12828
12914
  const labelId = react.useId();
12829
12915
  const styles = useStyles6();
@@ -12837,6 +12923,7 @@ function ThemeColorSelector({
12837
12923
  },
12838
12924
  [t, accentLabels]
12839
12925
  );
12926
+ if (accentColorLocked) return null;
12840
12927
  const handleOptionSelect = (_event, data) => {
12841
12928
  const next = data.optionValue;
12842
12929
  if (next && availableAccents.includes(next)) {
@@ -13004,7 +13091,304 @@ function LanguageDropdown({
13004
13091
  }
13005
13092
  );
13006
13093
  }
13094
+ var MINIMUM_QUERY_LENGTH = 3;
13095
+ var MAXIMUM_RESULTS = 50;
13096
+ var SEARCH_DEBOUNCE_MS4 = 300;
13097
+ var GUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13007
13098
  var useStyles7 = reactComponents.makeStyles({
13099
+ surface: {
13100
+ maxWidth: "480px",
13101
+ display: "flex",
13102
+ flexDirection: "column"
13103
+ },
13104
+ titleRow: {
13105
+ display: "flex",
13106
+ flexDirection: "row",
13107
+ alignItems: "flex-start",
13108
+ columnGap: reactComponents.tokens.spacingHorizontalS
13109
+ },
13110
+ titleText: {
13111
+ flexGrow: 1,
13112
+ minWidth: 0
13113
+ },
13114
+ body: {
13115
+ display: "flex",
13116
+ flexDirection: "column",
13117
+ rowGap: reactComponents.tokens.spacingVerticalM,
13118
+ paddingTop: reactComponents.tokens.spacingVerticalM
13119
+ },
13120
+ results: {
13121
+ display: "flex",
13122
+ flexDirection: "column",
13123
+ // Fixed height, not min/max: a list that resizes on every keystroke makes the confirm button
13124
+ // jump around under the pointer while the administrator is still reading.
13125
+ height: "280px",
13126
+ overflowY: "auto",
13127
+ // Full shorthand — griffel types `borderColor` and friends as `never`, so the longhand
13128
+ // properties aren't usable here.
13129
+ border: `1px solid ${reactComponents.tokens.colorNeutralStroke1}`,
13130
+ borderRadius: reactComponents.tokens.borderRadiusMedium,
13131
+ backgroundColor: reactComponents.tokens.colorNeutralBackground1
13132
+ },
13133
+ result: {
13134
+ display: "flex",
13135
+ flexDirection: "row",
13136
+ alignItems: "center",
13137
+ columnGap: reactComponents.tokens.spacingHorizontalS,
13138
+ padding: `${reactComponents.tokens.spacingVerticalS} ${reactComponents.tokens.spacingHorizontalM}`,
13139
+ cursor: "pointer",
13140
+ borderBottom: `1px solid ${reactComponents.tokens.colorNeutralStroke2}`,
13141
+ textAlign: "left",
13142
+ background: "none",
13143
+ // A native <button> does NOT inherit colour or font from its container — the user-agent
13144
+ // stylesheet gives it `color: buttontext` and its own font stack. Without these three the
13145
+ // rows rendered as near-black text on the dark surface in dark mode, which is how this was
13146
+ // first spotted. Set explicitly rather than relying on inheritance.
13147
+ color: reactComponents.tokens.colorNeutralForeground1,
13148
+ fontFamily: "inherit",
13149
+ fontSize: reactComponents.tokens.fontSizeBase300,
13150
+ // The UA also gives buttons a border; `background: none` alone leaves it drawing a box
13151
+ // around every row. Only three sides — borderBottom above is the row separator.
13152
+ borderTop: "none",
13153
+ borderRight: "none",
13154
+ borderLeft: "none",
13155
+ ":hover": {
13156
+ backgroundColor: reactComponents.tokens.colorNeutralBackground1Hover,
13157
+ color: reactComponents.tokens.colorNeutralForeground1Hover
13158
+ },
13159
+ ":focus-visible": {
13160
+ outline: `2px solid ${reactComponents.tokens.colorStrokeFocus2}`,
13161
+ outlineOffset: "-2px"
13162
+ }
13163
+ },
13164
+ resultSelected: {
13165
+ backgroundColor: reactComponents.tokens.colorBrandBackground,
13166
+ color: reactComponents.tokens.colorNeutralForegroundOnBrand,
13167
+ ":hover": {
13168
+ backgroundColor: reactComponents.tokens.colorBrandBackgroundHover,
13169
+ // Repeated because the base row's :hover also sets colour, and whichever griffel emits
13170
+ // last would otherwise win — leaving selected-and-hovered text unreadable on the brand fill.
13171
+ color: reactComponents.tokens.colorNeutralForegroundOnBrand
13172
+ }
13173
+ },
13174
+ resultText: {
13175
+ display: "flex",
13176
+ flexDirection: "column",
13177
+ minWidth: 0
13178
+ },
13179
+ name: {
13180
+ fontSize: reactComponents.tokens.fontSizeBase300,
13181
+ overflow: "hidden",
13182
+ textOverflow: "ellipsis",
13183
+ whiteSpace: "nowrap"
13184
+ },
13185
+ email: {
13186
+ fontSize: reactComponents.tokens.fontSizeBase200,
13187
+ opacity: 0.8,
13188
+ overflow: "hidden",
13189
+ textOverflow: "ellipsis",
13190
+ whiteSpace: "nowrap"
13191
+ },
13192
+ status: {
13193
+ display: "flex",
13194
+ flexDirection: "row",
13195
+ alignItems: "center",
13196
+ columnGap: reactComponents.tokens.spacingHorizontalS,
13197
+ padding: reactComponents.tokens.spacingVerticalXL,
13198
+ color: reactComponents.tokens.colorNeutralForeground3,
13199
+ fontSize: reactComponents.tokens.fontSizeBase200
13200
+ },
13201
+ warning: {
13202
+ display: "flex",
13203
+ flexDirection: "row",
13204
+ alignItems: "flex-start",
13205
+ columnGap: reactComponents.tokens.spacingHorizontalS,
13206
+ padding: `${reactComponents.tokens.spacingVerticalS} ${reactComponents.tokens.spacingHorizontalM}`,
13207
+ borderRadius: reactComponents.tokens.borderRadiusMedium,
13208
+ backgroundColor: reactComponents.tokens.colorNeutralBackground2,
13209
+ borderLeft: `3px solid ${reactComponents.tokens.colorPaletteRedBackground3}`,
13210
+ fontSize: reactComponents.tokens.fontSizeBase200
13211
+ },
13212
+ footer: {
13213
+ display: "flex",
13214
+ flexDirection: "row",
13215
+ justifyContent: "flex-end",
13216
+ columnGap: reactComponents.tokens.spacingHorizontalS,
13217
+ paddingTop: reactComponents.tokens.spacingVerticalM
13218
+ }
13219
+ });
13220
+ function ImpersonationPickerDialog({
13221
+ open,
13222
+ onClose,
13223
+ returnUrl = "/",
13224
+ className
13225
+ }) {
13226
+ const auth = react$1.useAuth();
13227
+ const styles = useStyles7();
13228
+ const t = react$1.useT();
13229
+ const { searchImpersonationTargets, impersonate } = auth;
13230
+ const [query, setQuery] = react.useState("");
13231
+ const [results, setResults] = react.useState([]);
13232
+ const [selected, setSelected] = react.useState(null);
13233
+ const [isSearching, setIsSearching] = react.useState(false);
13234
+ const [hasSearched, setHasSearched] = react.useState(false);
13235
+ const [hasMore, setHasMore] = react.useState(false);
13236
+ const [isStarting, setIsStarting] = react.useState(false);
13237
+ const requestIdRef = react.useRef(0);
13238
+ react.useEffect(() => {
13239
+ if (open) return;
13240
+ setQuery("");
13241
+ setResults([]);
13242
+ setSelected(null);
13243
+ setIsSearching(false);
13244
+ setHasSearched(false);
13245
+ setHasMore(false);
13246
+ setIsStarting(false);
13247
+ }, [open]);
13248
+ const trimmed = query.trim();
13249
+ const isTooShort = trimmed.length > 0 && trimmed.length < MINIMUM_QUERY_LENGTH;
13250
+ react.useEffect(() => {
13251
+ if (!open) return;
13252
+ const requestId = ++requestIdRef.current;
13253
+ if (trimmed.length < MINIMUM_QUERY_LENGTH && !GUID_PATTERN.test(trimmed)) {
13254
+ setResults([]);
13255
+ setHasSearched(false);
13256
+ setHasMore(false);
13257
+ setIsSearching(false);
13258
+ return;
13259
+ }
13260
+ let cancelled = false;
13261
+ const timer = setTimeout(async () => {
13262
+ setIsSearching(true);
13263
+ try {
13264
+ const response = await searchImpersonationTargets(trimmed);
13265
+ if (cancelled || requestId !== requestIdRef.current) return;
13266
+ setResults(response.results ?? []);
13267
+ setHasMore(response.hasMore ?? false);
13268
+ setHasSearched(true);
13269
+ } catch {
13270
+ if (cancelled || requestId !== requestIdRef.current) return;
13271
+ setResults([]);
13272
+ setHasMore(false);
13273
+ setHasSearched(true);
13274
+ } finally {
13275
+ if (!cancelled && requestId === requestIdRef.current) {
13276
+ setIsSearching(false);
13277
+ }
13278
+ }
13279
+ }, SEARCH_DEBOUNCE_MS4);
13280
+ return () => {
13281
+ cancelled = true;
13282
+ clearTimeout(timer);
13283
+ };
13284
+ }, [open, searchImpersonationTargets, trimmed]);
13285
+ const handleQueryChange = react.useCallback((value) => {
13286
+ setQuery(value);
13287
+ setSelected(null);
13288
+ }, []);
13289
+ const handleConfirm = react.useCallback(async () => {
13290
+ if (!selected?.contactId) return;
13291
+ setIsStarting(true);
13292
+ try {
13293
+ const response = await impersonate(selected.contactId);
13294
+ if (response.result === core.ImpersonateResult.Started) {
13295
+ window.location.assign(returnUrl);
13296
+ return;
13297
+ }
13298
+ setIsStarting(false);
13299
+ onClose();
13300
+ } catch {
13301
+ setIsStarting(false);
13302
+ }
13303
+ }, [impersonate, onClose, returnUrl, selected]);
13304
+ return /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Dialog, { open, onOpenChange: (_, data) => {
13305
+ if (!data.open) onClose();
13306
+ }, children: /* @__PURE__ */ jsxRuntime.jsxs(
13307
+ reactComponents.DialogSurface,
13308
+ {
13309
+ className: reactComponents.mergeClasses("ppp-impersonation-picker-dialog", styles.surface, className),
13310
+ children: [
13311
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.titleRow, children: [
13312
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DialogTitle, { className: styles.titleText, children: t("app.components.impersonation-picker.title") }),
13313
+ /* @__PURE__ */ jsxRuntime.jsx(
13314
+ reactComponents.Button,
13315
+ {
13316
+ appearance: "subtle",
13317
+ icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.Dismiss24Regular, {}),
13318
+ "aria-label": t("app.components.impersonation-picker.cancel"),
13319
+ onClick: onClose
13320
+ }
13321
+ )
13322
+ ] }),
13323
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.body, children: [
13324
+ /* @__PURE__ */ jsxRuntime.jsx(
13325
+ reactComponents.SearchBox,
13326
+ {
13327
+ value: query,
13328
+ onChange: (_, data) => handleQueryChange(data.value),
13329
+ placeholder: t("app.components.impersonation-picker.search-placeholder"),
13330
+ appearance: "outline",
13331
+ style: { width: "100%" }
13332
+ }
13333
+ ),
13334
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.results, role: "listbox", children: isSearching ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.status, children: [
13335
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Spinner, { size: "tiny" }),
13336
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("app.components.impersonation-picker.searching") })
13337
+ ] }) : isTooShort ? (
13338
+ // Distinct from "no matches": the search hasn't run. Conflating the two reads as
13339
+ // "this person doesn't exist" when the user has simply typed too few characters.
13340
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.status, children: t("app.components.impersonation-picker.hint", [String(MINIMUM_QUERY_LENGTH)]) })
13341
+ ) : hasSearched && results.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.status, children: t("app.components.impersonation-picker.no-matches") }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
13342
+ results.map((candidate) => {
13343
+ const isSelected = selected?.contactId === candidate.contactId;
13344
+ return /* @__PURE__ */ jsxRuntime.jsxs(
13345
+ "button",
13346
+ {
13347
+ type: "button",
13348
+ role: "option",
13349
+ "aria-selected": isSelected,
13350
+ className: reactComponents.mergeClasses(
13351
+ styles.result,
13352
+ isSelected && styles.resultSelected
13353
+ ),
13354
+ onClick: () => setSelected(candidate),
13355
+ children: [
13356
+ /* @__PURE__ */ jsxRuntime.jsx(reactIcons.Person20Regular, {}),
13357
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles.resultText, children: [
13358
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.name, children: candidate.fullName ?? candidate.email ?? candidate.contactId }),
13359
+ candidate.email && /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.email, children: candidate.email })
13360
+ ] })
13361
+ ]
13362
+ },
13363
+ candidate.contactId
13364
+ );
13365
+ }),
13366
+ hasMore && /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.status, children: t("app.components.impersonation-picker.too-many", [String(MAXIMUM_RESULTS)]) })
13367
+ ] }) }),
13368
+ selected && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.warning, children: [
13369
+ /* @__PURE__ */ jsxRuntime.jsx(reactIcons.Warning20Regular, {}),
13370
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("app.components.impersonation-picker.warning") })
13371
+ ] })
13372
+ ] }),
13373
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.footer, children: [
13374
+ /* @__PURE__ */ jsxRuntime.jsx(
13375
+ reactComponents.Button,
13376
+ {
13377
+ appearance: "primary",
13378
+ icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.EyeTracking20Regular, {}),
13379
+ disabled: !selected || isStarting,
13380
+ onClick: handleConfirm,
13381
+ children: t("app.components.impersonation-picker.confirm")
13382
+ }
13383
+ ),
13384
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Button, { appearance: "secondary", onClick: onClose, disabled: isStarting, children: t("app.components.impersonation-picker.cancel") })
13385
+ ] })
13386
+ ]
13387
+ }
13388
+ ) });
13389
+ }
13390
+ var IMPERSONATION_ROLE = "SystemAdmin";
13391
+ var useStyles8 = reactComponents.makeStyles({
13008
13392
  trigger: {
13009
13393
  backgroundColor: "transparent",
13010
13394
  border: "none",
@@ -13064,8 +13448,9 @@ function ProfileMainMenu({
13064
13448
  signInUrl = "/login"
13065
13449
  }) {
13066
13450
  const auth = react$1.useAuth();
13067
- const styles = useStyles7();
13451
+ const styles = useStyles8();
13068
13452
  const t = react$1.useT();
13453
+ const [isPickerOpen, setIsPickerOpen] = react.useState(false);
13069
13454
  const handleLogout = react.useCallback(async () => {
13070
13455
  try {
13071
13456
  await auth.logout();
@@ -13082,7 +13467,14 @@ function ProfileMainMenu({
13082
13467
  }
13083
13468
  }, [auth]);
13084
13469
  if (auth.status === react$1.AuthStatus.Loading) {
13085
- return /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Avatar, { className: "ppp-profile-main-menu", size: 28, "aria-label": t("app.messages.record-loading") });
13470
+ return /* @__PURE__ */ jsxRuntime.jsx(
13471
+ reactComponents.Avatar,
13472
+ {
13473
+ className: "ppp-profile-main-menu",
13474
+ size: 28,
13475
+ "aria-label": t("app.messages.record-loading")
13476
+ }
13477
+ );
13086
13478
  }
13087
13479
  if (auth.status === react$1.AuthStatus.Anonymous) {
13088
13480
  if (anonymousContent !== void 0) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: anonymousContent });
@@ -13103,61 +13495,126 @@ function ProfileMainMenu({
13103
13495
  const resolvedAccountLabel = accountLabel ?? t("app.components.profile-main-menu.account");
13104
13496
  const signedInAsLabel = t("app.components.profile-main-menu.signed-in-as");
13105
13497
  const logoutLabel = t("app.buttons.logout.label");
13498
+ const canImpersonate = (auth.user.roles?.includes(IMPERSONATION_ROLE) ?? false) && auth.user.portalUserType === core.PortalUserType.SystemUser;
13106
13499
  const altIdentityTableName = auth.user.altIdentityTableName;
13107
- const switchIdentityLabel = altIdentityTableName ? t(
13108
- "app.components.profile-main-menu.switch-to",
13109
- [
13110
- altIdentityTableName === "systemuser" ? t("app.components.profile-main-menu.alt-kind-systemuser") : t("app.components.profile-main-menu.alt-kind-contact")
13111
- ]
13112
- ) : null;
13113
- return /* @__PURE__ */ jsxRuntime.jsxs(reactComponents.Menu, { children: [
13114
- /* @__PURE__ */ jsxRuntime.jsx(reactComponents.MenuTrigger, { disableButtonEnhancement: true, children: /* @__PURE__ */ jsxRuntime.jsx(
13115
- "button",
13500
+ const switchIdentityLabel = altIdentityTableName ? t("app.components.profile-main-menu.switch-to", [
13501
+ altIdentityTableName === "systemuser" ? t("app.components.profile-main-menu.alt-kind-systemuser") : t("app.components.profile-main-menu.alt-kind-contact")
13502
+ ]) : null;
13503
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
13504
+ /* @__PURE__ */ jsxRuntime.jsxs(reactComponents.Menu, { children: [
13505
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.MenuTrigger, { disableButtonEnhancement: true, children: /* @__PURE__ */ jsxRuntime.jsx(
13506
+ "button",
13507
+ {
13508
+ type: "button",
13509
+ className: reactComponents.mergeClasses("ppp-profile-main-menu", styles.trigger),
13510
+ "aria-label": email || logoutLabel,
13511
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Avatar, { size: 28, initials, name: email })
13512
+ }
13513
+ ) }),
13514
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.MenuPopover, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.popoverBody, children: [
13515
+ headerSlot,
13516
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.identity, children: [
13517
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Avatar, { size: 40, initials, name: email }),
13518
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.identityText, children: [
13519
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.identityLabel, children: signedInAsLabel }),
13520
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.identityValue, title: email, children: email })
13521
+ ] })
13522
+ ] }),
13523
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Divider, {}),
13524
+ /* @__PURE__ */ jsxRuntime.jsxs(reactComponents.MenuList, { className: styles.actions, children: [
13525
+ accountUrl && /* @__PURE__ */ jsxRuntime.jsx(
13526
+ reactComponents.MenuItem,
13527
+ {
13528
+ icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.PersonEdit20Regular, {}),
13529
+ onClick: () => {
13530
+ window.location.href = accountUrl;
13531
+ },
13532
+ children: resolvedAccountLabel
13533
+ }
13534
+ ),
13535
+ switchIdentityLabel && /* @__PURE__ */ jsxRuntime.jsx(
13536
+ reactComponents.MenuItem,
13537
+ {
13538
+ icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.ArrowSwap20Regular, {}),
13539
+ onClick: handleSwitchIdentity,
13540
+ children: switchIdentityLabel
13541
+ }
13542
+ ),
13543
+ canImpersonate && /* @__PURE__ */ jsxRuntime.jsx(
13544
+ reactComponents.MenuItem,
13545
+ {
13546
+ icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.EyeTracking20Regular, {}),
13547
+ onClick: () => setIsPickerOpen(true),
13548
+ children: t("app.components.profile-main-menu.impersonate")
13549
+ }
13550
+ ),
13551
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.MenuItem, { icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.SignOut20Regular, {}), onClick: handleLogout, children: logoutLabel })
13552
+ ] }),
13553
+ footerSlot && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
13554
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Divider, {}),
13555
+ footerSlot
13556
+ ] })
13557
+ ] }) })
13558
+ ] }),
13559
+ canImpersonate && /* @__PURE__ */ jsxRuntime.jsx(
13560
+ ImpersonationPickerDialog,
13116
13561
  {
13117
- type: "button",
13118
- className: reactComponents.mergeClasses("ppp-profile-main-menu", styles.trigger),
13119
- "aria-label": email || logoutLabel,
13120
- children: /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Avatar, { size: 28, initials, name: email })
13562
+ open: isPickerOpen,
13563
+ onClose: () => setIsPickerOpen(false)
13121
13564
  }
13122
- ) }),
13123
- /* @__PURE__ */ jsxRuntime.jsx(reactComponents.MenuPopover, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.popoverBody, children: [
13124
- headerSlot,
13125
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.identity, children: [
13126
- /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Avatar, { size: 40, initials, name: email }),
13127
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.identityText, children: [
13128
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.identityLabel, children: signedInAsLabel }),
13129
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.identityValue, title: email, children: email })
13130
- ] })
13131
- ] }),
13132
- /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Divider, {}),
13133
- /* @__PURE__ */ jsxRuntime.jsxs(reactComponents.MenuList, { className: styles.actions, children: [
13134
- accountUrl && /* @__PURE__ */ jsxRuntime.jsx(
13135
- reactComponents.MenuItem,
13136
- {
13137
- icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.PersonEdit20Regular, {}),
13138
- onClick: () => {
13139
- window.location.href = accountUrl;
13140
- },
13141
- children: resolvedAccountLabel
13142
- }
13143
- ),
13144
- switchIdentityLabel && /* @__PURE__ */ jsxRuntime.jsx(
13145
- reactComponents.MenuItem,
13146
- {
13147
- icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.ArrowSwap20Regular, {}),
13148
- onClick: handleSwitchIdentity,
13149
- children: switchIdentityLabel
13150
- }
13151
- ),
13152
- /* @__PURE__ */ jsxRuntime.jsx(reactComponents.MenuItem, { icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.SignOut20Regular, {}), onClick: handleLogout, children: logoutLabel })
13153
- ] }),
13154
- footerSlot && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
13155
- /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Divider, {}),
13156
- footerSlot
13157
- ] })
13158
- ] }) })
13565
+ )
13159
13566
  ] });
13160
13567
  }
13568
+ function ImpersonationBanner({
13569
+ returnUrl = "/",
13570
+ className
13571
+ }) {
13572
+ const auth = react$1.useAuth();
13573
+ const t = react$1.useT();
13574
+ const [isStopping, setIsStopping] = react.useState(false);
13575
+ const { stopImpersonation } = auth;
13576
+ const handleStop = react.useCallback(async () => {
13577
+ setIsStopping(true);
13578
+ try {
13579
+ await stopImpersonation();
13580
+ window.location.assign(returnUrl);
13581
+ } catch {
13582
+ setIsStopping(false);
13583
+ }
13584
+ }, [returnUrl, stopImpersonation]);
13585
+ if (auth.status !== react$1.AuthStatus.Authenticated || !auth.user.isImpersonating) {
13586
+ return null;
13587
+ }
13588
+ const viewingAs = auth.user.email ?? auth.user.userName ?? "";
13589
+ const impersonator = auth.user.impersonatorName;
13590
+ return (
13591
+ // The class is placement only — PageLayout targets it to assign the grid row. It carries no
13592
+ // appearance of its own; Fluent owns all of that.
13593
+ /* @__PURE__ */ jsxRuntime.jsxs(
13594
+ reactComponents.MessageBar,
13595
+ {
13596
+ className: reactComponents.mergeClasses("ppp-impersonation-banner", className),
13597
+ intent: "error",
13598
+ children: [
13599
+ /* @__PURE__ */ jsxRuntime.jsxs(reactComponents.MessageBarBody, { children: [
13600
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.MessageBarTitle, { children: t("app.components.impersonation-banner.viewing-as", [viewingAs]) }),
13601
+ impersonator ? ` ${t("app.components.impersonation-banner.you-are", [impersonator])}` : null
13602
+ ] }),
13603
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.MessageBarActions, { children: /* @__PURE__ */ jsxRuntime.jsx(
13604
+ reactComponents.Button,
13605
+ {
13606
+ icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.ArrowExit20Regular, {}),
13607
+ iconPosition: "after",
13608
+ disabled: isStopping,
13609
+ onClick: handleStop,
13610
+ children: t("app.components.impersonation-banner.stop")
13611
+ }
13612
+ ) })
13613
+ ]
13614
+ }
13615
+ )
13616
+ );
13617
+ }
13161
13618
  var usePanelStyles = reactComponents.makeStyles({
13162
13619
  // Vertical stack of setting sections with consistent gap. Matches the
13163
13620
  // panel body the SiteSettingsButton's drawer renders, but extracted
@@ -13206,7 +13663,7 @@ function SiteSettingsPanel({
13206
13663
  }
13207
13664
  );
13208
13665
  }
13209
- var useStyles8 = reactComponents.makeStyles({
13666
+ var useStyles9 = reactComponents.makeStyles({
13210
13667
  // Wraps the standalone panel inside the drawer body to add the small
13211
13668
  // top inset that the popover's title visually expects. The panel
13212
13669
  // itself stays inset-free so it doesn't carry extra padding when
@@ -13226,7 +13683,7 @@ function SiteSettingsButton({
13226
13683
  accentLabels
13227
13684
  }) {
13228
13685
  const t = react$1.useT();
13229
- const styles = useStyles8();
13686
+ const styles = useStyles9();
13230
13687
  const [open, setOpen] = react.useState(false);
13231
13688
  const close = react.useCallback(() => setOpen(false), []);
13232
13689
  const label = t("app.site-settings-label");
@@ -13704,7 +14161,7 @@ function bestRouteMatch(routes, pathname) {
13704
14161
  }
13705
14162
  return best;
13706
14163
  }
13707
- var useStyles9 = reactComponents.makeStyles({
14164
+ var useStyles10 = reactComponents.makeStyles({
13708
14165
  // Shared scrim look. Both global (full-screen) and scoped variants reuse
13709
14166
  // these so the visual feel is identical — only the positioning differs.
13710
14167
  // Theme-aware translucent backdrop drawn via a `::before` pseudo-element
@@ -13766,7 +14223,7 @@ function FluentOverlayProvider({ children }) {
13766
14223
  ] });
13767
14224
  }
13768
14225
  function GlobalOverlayLayer() {
13769
- const styles = useStyles9();
14226
+ const styles = useStyles10();
13770
14227
  const overlay = react$1.useOverlayForTarget(void 0);
13771
14228
  if (!overlay) return null;
13772
14229
  return /* @__PURE__ */ jsxRuntime.jsx(ScrimContent, { overlay, className: reactComponents.mergeClasses(styles.scrim, styles.global) });
@@ -13777,7 +14234,7 @@ function FluentOverlayTarget({
13777
14234
  className,
13778
14235
  style
13779
14236
  }) {
13780
- const styles = useStyles9();
14237
+ const styles = useStyles10();
13781
14238
  const overlay = react$1.useOverlayForTarget(targetId);
13782
14239
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: reactComponents.mergeClasses("ppp-overlay-target", styles.scopedHost, className), style, children: [
13783
14240
  children,
@@ -13785,14 +14242,14 @@ function FluentOverlayTarget({
13785
14242
  ] });
13786
14243
  }
13787
14244
  function ScrimContent({ overlay, className }) {
13788
- const styles = useStyles9();
14245
+ const styles = useStyles10();
13789
14246
  const showProgress = overlay.showProgress !== false;
13790
14247
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, role: "status", "aria-live": "polite", children: [
13791
14248
  showProgress && /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Spinner, { size: "large" }),
13792
14249
  overlay.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.description, children: overlay.description })
13793
14250
  ] });
13794
14251
  }
13795
- var useStyles10 = reactComponents.makeStyles({
14252
+ var useStyles11 = reactComponents.makeStyles({
13796
14253
  // Centered placeholder used while prefixes are being fetched. Same
13797
14254
  // shape as the spinner blocks in `<NewRecordGridButton>` /
13798
14255
  // `<OpenRecordGridButton>` dialogs (~160px min height + vertical
@@ -13812,7 +14269,7 @@ function LocalizationBoundary({
13812
14269
  fallback,
13813
14270
  children
13814
14271
  }) {
13815
- const styles = useStyles10();
14272
+ const styles = useStyles11();
13816
14273
  const resolvedPrefixes = prefixes ?? EMPTY_PREFIXES;
13817
14274
  const { locale } = react$1.useLocale();
13818
14275
  react$1.useLocalization(resolvedPrefixes);
@@ -13834,7 +14291,7 @@ function LocalizationBoundary({
13834
14291
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: reactComponents.mergeClasses("ppp-localization-boundary", styles.fallback), role: "status", "aria-live": "polite", children: /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Spinner, { size: "medium" }) });
13835
14292
  }
13836
14293
  var EMPTY_PREFIXES = Object.freeze([]);
13837
- var useStyles11 = reactComponents.makeStyles({
14294
+ var useStyles12 = reactComponents.makeStyles({
13838
14295
  root: {
13839
14296
  display: "flex",
13840
14297
  flexDirection: "column",
@@ -13892,7 +14349,7 @@ function isSameLanguage(a, b) {
13892
14349
  return primaryA.length > 0 && primaryA === primary(b);
13893
14350
  }
13894
14351
  function LocalizationTranslator({ className }) {
13895
- const styles = useStyles11();
14352
+ const styles = useStyles12();
13896
14353
  const t = react$1.useT();
13897
14354
  const { ppp } = react$1.usePowerPortalsPro();
13898
14355
  const [availability, setAvailability] = react.useState(null);
@@ -14380,7 +14837,7 @@ function SectionColumnRenderer({
14380
14837
  }
14381
14838
  );
14382
14839
  }
14383
- var useStyles12 = reactComponents.makeStyles({
14840
+ var useStyles13 = reactComponents.makeStyles({
14384
14841
  root: {
14385
14842
  display: "flex",
14386
14843
  flexDirection: "column",
@@ -14460,6 +14917,18 @@ var useStyles12 = reactComponents.makeStyles({
14460
14917
  },
14461
14918
  // Scroll viewport for the merged grid so a tall result set scrolls inside the dialog.
14462
14919
  dialogGrid: { maxHeight: "60vh", overflowY: "auto", overflowX: "auto" },
14920
+ // Merged-snapshot cells. Keys and sources are long single tokens (dotted keys,
14921
+ // backslash-only absolute paths) with no natural break opportunity, so without
14922
+ // this the text paints straight over the neighbouring column. `overflow-wrap:
14923
+ // anywhere` (not just `break-word`) is what makes the flex cell honour its
14924
+ // column width — it counts the soft breaks toward min-content — so the token
14925
+ // wraps inside the column instead of stretching it. Same recipe as
14926
+ // `sourceCell` in the per-source loads table above.
14927
+ mergedCell: {
14928
+ whiteSpace: "normal",
14929
+ overflowWrap: "anywhere",
14930
+ wordBreak: "break-word"
14931
+ },
14463
14932
  filterInput: { minWidth: "260px" },
14464
14933
  pager: { display: "flex", alignItems: "center", gap: reactComponents.tokens.spacingHorizontalS },
14465
14934
  grow: { flex: 1 },
@@ -14491,7 +14960,7 @@ function cultureLabel(culture) {
14491
14960
  }
14492
14961
  }
14493
14962
  function LocalizationAdmin({ className }) {
14494
- const styles = useStyles12();
14963
+ const styles = useStyles13();
14495
14964
  const t = react$1.useT();
14496
14965
  const { ppp } = react$1.usePowerPortalsPro();
14497
14966
  const [overview, setOverview] = react.useState(null);
@@ -14941,7 +15410,7 @@ function MergedSourcesDialog({
14941
15410
  size: "small",
14942
15411
  children: [
14943
15412
  /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DataGridHeader, { children: /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DataGridRow, { children: ({ renderHeaderCell }) => /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DataGridHeaderCell, { children: renderHeaderCell() }) }) }),
14944
- /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DataGridBody, { children: ({ item, rowId }) => /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DataGridRow, { children: ({ renderCell }) => /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DataGridCell, { children: renderCell(item) }) }, rowId) })
15413
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DataGridBody, { children: ({ item, rowId }) => /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DataGridRow, { children: ({ renderCell }) => /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DataGridCell, { className: styles.mergedCell, children: renderCell(item) }) }, rowId) })
14945
15414
  ]
14946
15415
  }
14947
15416
  ) })
@@ -14961,7 +15430,7 @@ function MergedSourcesDialog({
14961
15430
  ] })
14962
15431
  ] }) }) });
14963
15432
  }
14964
- var useStyles13 = reactComponents.makeStyles({
15433
+ var useStyles14 = reactComponents.makeStyles({
14965
15434
  root: {
14966
15435
  display: "flex",
14967
15436
  flexDirection: "column",
@@ -14984,7 +15453,7 @@ var useStyles13 = reactComponents.makeStyles({
14984
15453
  });
14985
15454
  var toMs = (value) => Number(value);
14986
15455
  function CacheAdmin({ className }) {
14987
- const styles = useStyles13();
15456
+ const styles = useStyles14();
14988
15457
  const t = react$1.useT();
14989
15458
  const { ppp } = react$1.usePowerPortalsPro();
14990
15459
  const [cacheNames, setCacheNames] = react.useState([]);
@@ -15119,7 +15588,7 @@ function CacheAdmin({ className }) {
15119
15588
  ] }) : null
15120
15589
  ] });
15121
15590
  }
15122
- var useStyles14 = reactComponents.makeStyles({
15591
+ var useStyles15 = reactComponents.makeStyles({
15123
15592
  list: {
15124
15593
  margin: 0,
15125
15594
  paddingLeft: reactComponents.tokens.spacingHorizontalXL,
@@ -15134,7 +15603,7 @@ function ValidationSummary({
15134
15603
  className,
15135
15604
  style
15136
15605
  }) {
15137
- const styles = useStyles14();
15606
+ const styles = useStyles15();
15138
15607
  const t = react$1.useT();
15139
15608
  const validation = react$1.useValidationContext();
15140
15609
  const resolvedTitle = title === void 0 ? t("app.errors.form-validation-errors") : title;
@@ -16011,7 +16480,7 @@ function isWizardRecordPageElement(node) {
16011
16480
  const type = node.type;
16012
16481
  return !!type && type[PAGE_MARKER] === true;
16013
16482
  }
16014
- var useStyles15 = reactComponents.makeStyles({
16483
+ var useStyles16 = reactComponents.makeStyles({
16015
16484
  shell: {
16016
16485
  display: "flex",
16017
16486
  flexDirection: "column",
@@ -16153,7 +16622,7 @@ function WizardRecordFormInner({
16153
16622
  stepperVisibility,
16154
16623
  isCreate
16155
16624
  }) {
16156
- const styles = useStyles15();
16625
+ const styles = useStyles16();
16157
16626
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: reactComponents.mergeClasses("ppp-wizard-record-form", styles.shell), children: /* @__PURE__ */ jsxRuntime.jsx(
16158
16627
  reactUseWizard.Wizard,
16159
16628
  {
@@ -16187,7 +16656,7 @@ function WizardFooter({
16187
16656
  onCancel,
16188
16657
  cancelLabel
16189
16658
  }) {
16190
- const styles = useStyles15();
16659
+ const styles = useStyles16();
16191
16660
  const t = react$1.useT();
16192
16661
  const recordContext = react$1.useRecordContext();
16193
16662
  const wizard = reactUseWizard.useWizard();
@@ -17669,13 +18138,17 @@ var usePageLayoutStyles = reactComponents.makeStyles({
17669
18138
  position: "relative",
17670
18139
  display: "grid",
17671
18140
  gridTemplateColumns: "auto minmax(0, 1fr)",
17672
- gridTemplateRows: "auto 1fr auto",
18141
+ // Four rows, not three: the first carries the impersonation banner. It's an `auto` track and
18142
+ // the banner renders nothing for an ordinary session, so the row collapses to zero height and
18143
+ // the layout is unchanged whenever nobody is impersonating.
18144
+ gridTemplateRows: "auto auto 1fr auto",
17673
18145
  // Lets the PageLayout shrink when it's nested inside another
17674
18146
  // PageLayout's `<main>` (the outer main is a grid item; without
17675
18147
  // `min-width: 0` on this inner root the outer's `minmax(0, 1fr)`
17676
18148
  // track wouldn't be honored once content tries to grow wider).
17677
18149
  minWidth: 0,
17678
18150
  gridTemplateAreas: `
18151
+ 'impersonation impersonation'
17679
18152
  'header header'
17680
18153
  'navigation main'
17681
18154
  'footer footer'
@@ -17785,6 +18258,11 @@ var usePageLayoutStyles = reactComponents.makeStyles({
17785
18258
  childHeader: {
17786
18259
  borderBottomWidth: "1px"
17787
18260
  },
18261
+ impersonationBanner: {
18262
+ // Placement only. The banner is a Fluent MessageBar and Fluent owns everything about how it
18263
+ // looks; this grid assignment is the extent of PageLayout's involvement.
18264
+ gridArea: "impersonation"
18265
+ },
17788
18266
  navigation: {
17789
18267
  gridArea: "navigation",
17790
18268
  overflowY: "auto",
@@ -17911,6 +18389,7 @@ function PageLayout({
17911
18389
  ...resizable ? { "--ppp-nav-width": `${navWidth}px` } : {}
17912
18390
  };
17913
18391
  return /* @__PURE__ */ jsxRuntime.jsx(PageLayoutNestingContext.Provider, { value: { isNested: true }, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: rootClass, style: rootStyle, children: [
18392
+ !isNested && /* @__PURE__ */ jsxRuntime.jsx(ImpersonationBanner, { className: styles.impersonationBanner }),
17914
18393
  header !== void 0 && headerVisible && /* @__PURE__ */ jsxRuntime.jsx(Header, { className: headerClass, ...headerStyle && { style: headerStyle }, children: header }),
17915
18394
  navigation !== void 0 && navigationVisible && /* @__PURE__ */ jsxRuntime.jsx(
17916
18395
  "nav",
@@ -18139,7 +18618,7 @@ var RADIAL_CHART_TYPES = /* @__PURE__ */ new Set([
18139
18618
  reactCharts.ChartType.Radar,
18140
18619
  reactCharts.ChartType.Funnel
18141
18620
  ]);
18142
- var useStyles16 = reactComponents.makeStyles({
18621
+ var useStyles17 = reactComponents.makeStyles({
18143
18622
  // The wrapper uses Fluent design tokens directly so the rounded bordered
18144
18623
  // look picks up theme changes. `colorNeutralBackground1` (white in light,
18145
18624
  // grey[16] in dark) matches the Blazor FluentUIChart's
@@ -18217,7 +18696,7 @@ function FluentUIChartImpl(props, ref) {
18217
18696
  showBorder = true,
18218
18697
  onElementClick
18219
18698
  } = props;
18220
- const styles = useStyles16();
18699
+ const styles = useStyles17();
18221
18700
  const containerRef = react.useRef(null);
18222
18701
  const palette = useFluentChartPalette(containerRef);
18223
18702
  const themedDatasets = react.useMemo(() => {
@@ -18298,7 +18777,7 @@ function assembleTheme(palette, yAxisPrefix, yAxisSuffix, xAxisPrefix, xAxisSuff
18298
18777
  if (orientation !== void 0) theme.indexAxis = reactCharts.toIndexAxis(orientation);
18299
18778
  return Object.keys(theme).length > 0 ? theme : void 0;
18300
18779
  }
18301
- var useStyles17 = reactComponents.makeStyles({
18780
+ var useStyles18 = reactComponents.makeStyles({
18302
18781
  // Outer container — flex column so the view selector, message bar, and
18303
18782
  // chart fill share the caller's height budget.
18304
18783
  root: {
@@ -18376,7 +18855,7 @@ var DataverseChart = react.forwardRef(
18376
18855
  } = props;
18377
18856
  const chartSource = dataSource instanceof reactCharts.DataverseChartDataSource ? dataSource : void 0;
18378
18857
  const viewDs = dataSource instanceof reactCharts.DataverseChartDataSource ? void 0 : dataSource;
18379
- const styles = useStyles17();
18858
+ const styles = useStyles18();
18380
18859
  const t = react$1.useT();
18381
18860
  const { ppp, viewMetadataCache } = react$1.usePowerPortalsPro();
18382
18861
  const [data, setData] = react.useState({ labels: [], datasets: [] });
@@ -18646,6 +19125,8 @@ exports.Header = Header;
18646
19125
  exports.Highlighter = Highlighter;
18647
19126
  exports.ImageEdit = ImageEdit;
18648
19127
  exports.ImageViewer = ImageViewer;
19128
+ exports.ImpersonationBanner = ImpersonationBanner;
19129
+ exports.ImpersonationPickerDialog = ImpersonationPickerDialog;
18649
19130
  exports.LabelPosition = LabelPosition;
18650
19131
  exports.LanguageDropdown = LanguageDropdown;
18651
19132
  exports.LinkExistingRecordGridButton = LinkExistingRecordGridButton;