@powerportalspro/react-fluent 5.1.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -4635,7 +4635,11 @@ interface MainGridProps {
4635
4635
  * Set to `'virtualize'` for infinite-scroll — the pager footer is
4636
4636
  * hidden, rows accumulate across page boundaries, and the next page
4637
4637
  * fetches automatically when the user scrolls past the rendered
4638
- * bottom.
4638
+ * bottom. Pages are requested sequentially with the previous page's
4639
+ * Dataverse paging cookie (cursor paging), and loading stops when the
4640
+ * server reports no more records — not when the accumulated rows reach
4641
+ * the total count, which can never happen if per-row permission
4642
+ * handlers drop rows after Dataverse has counted them.
4639
4643
  */
4640
4644
  pagingMode?: PagingMode;
4641
4645
  /**
package/dist/index.d.ts CHANGED
@@ -4635,7 +4635,11 @@ interface MainGridProps {
4635
4635
  * Set to `'virtualize'` for infinite-scroll — the pager footer is
4636
4636
  * hidden, rows accumulate across page boundaries, and the next page
4637
4637
  * fetches automatically when the user scrolls past the rendered
4638
- * bottom.
4638
+ * bottom. Pages are requested sequentially with the previous page's
4639
+ * Dataverse paging cookie (cursor paging), and loading stops when the
4640
+ * server reports no more records — not when the accumulated rows reach
4641
+ * the total count, which can never happen if per-row permission
4642
+ * handlers drop rows after Dataverse has counted them.
4639
4643
  */
4640
4644
  pagingMode?: PagingMode;
4641
4645
  /**
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { makeStyles, mergeClasses, Input, tokens, Toolbar, ToolbarButton, ToolbarDivider, Menu, MenuTrigger, MenuPopover, MenuList, MenuItemRadio, Button, createLightTheme, createDarkTheme, makeStaticStyles, MessageBar, MessageBarBody, Dropdown, Option, Spinner, Field, Textarea, Switch, Checkbox, Label, TabList, Tab, Tooltip, Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, RadioGroup, Radio, useId as useId$1, Text, Overflow, OverflowItem, SearchBox, CounterBadge, Popover, PopoverTrigger, PopoverSurface, MenuItem, Combobox, TagPicker, TagPickerControl, TagPickerGroup, Tag, TagPickerInput, TagPickerList, TagPickerOption, webDarkTheme, webLightTheme, teamsDarkTheme, teamsLightTheme, FluentProvider, Avatar, Divider, OverlayDrawer, DrawerHeader, DrawerHeaderTitle, DrawerBody, useOverflowMenu, useIsOverflowItemVisible, Title3, Table, TableHeader, TableRow, TableHeaderCell, TableBody, TableCell, MessageBarTitle, DialogTrigger } from '@fluentui/react-components';
2
- import { MaskMode, useMaskedTextField, useT, usePowerPortalsPro, useRecordContext, useColumnMetadata, useLocale, MainContext, useTableMetadata, useWarnAboutUnsavedChanges, useMainContext, useViewsForTable, useViewMetadata, QueryStatus, useGridData, RecordContext, DialogServiceProvider, useUrlCultureStrategy, useDefaultLocale, useSupportedLocales, setAspNetCultureCookie, buildLocalePath, useAuth, AuthStatus, useOverlayServiceState, OverlayServiceProvider, useOverlayForTarget, useLocalization, useLocalizationLoaded, useValidationContext, useDialogService, useRunWithOverlay, useRecordContextOptional, useOrganizationSettings, useUnsavedChangesGuard, useColumnValidationErrors } from '@powerportalspro/react';
1
+ import { makeStyles, mergeClasses, Input, tokens, Toolbar, ToolbarButton, ToolbarDivider, Menu, MenuTrigger, MenuPopover, MenuList, MenuItemRadio, Button, createLightTheme, createDarkTheme, makeStaticStyles, MessageBar, MessageBarBody, Dropdown, Option, Spinner, Field, Textarea, Switch, Checkbox, Label, TabList, Tab, Tooltip, Dialog, DialogSurface, DialogBody, DialogTitle, DialogContent, DialogActions, RadioGroup, Radio, useId as useId$1, Text, Overflow, OverflowItem, SearchBox, CounterBadge, Popover, PopoverTrigger, PopoverSurface, MenuItem, Combobox, TagPicker, TagPickerControl, TagPickerGroup, Tag, TagPickerInput, TagPickerList, TagPickerOption, webDarkTheme, webLightTheme, teamsDarkTheme, teamsLightTheme, FluentProvider, Avatar, Divider, OverlayDrawer, DrawerHeader, DrawerHeaderTitle, DrawerBody, useOverflowMenu, useIsOverflowItemVisible, Title3, Table, TableHeader, TableRow, TableHeaderCell, TableBody, TableCell, createTableColumn, DataGrid, DataGridHeader, DataGridRow, DataGridHeaderCell, DataGridBody, DataGridCell, MessageBarTitle, DialogTrigger } from '@fluentui/react-components';
2
+ import { MaskMode, useMaskedTextField, useT, usePowerPortalsPro, useRecordContext, useColumnMetadata, useLocale, MainContext, useTableMetadata, useWarnAboutUnsavedChanges, useMainContext, useViewsForTable, useViewMetadata, QueryStatus, useGridData, RecordContext, DialogServiceProvider, useUrlCultureStrategy, useDefaultLocale, useSupportedLocales, setAspNetCultureCookie, buildLocalePath, useAuth, AuthStatus, useOverlayServiceState, OverlayServiceProvider, useOverlayForTarget, useLocalization, useLocalizationLoaded, useValidationContext, useDialogService, useRunWithOverlay, useRecordContextOptional, useOrganizationSettings, useUnsavedChangesGuard, useColumnValidationErrors, isKeyModeEnabled } from '@powerportalspro/react';
3
3
  import { forwardRef, useRef, useImperativeHandle, useState, useEffect, useCallback, createContext, useMemo, useId, useLayoutEffect, useContext, useTransition, Children, isValidElement } from 'react';
4
4
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
5
- import { ArrowRotateCounterclockwise20Regular, ArrowRotateClockwise20Regular, FlipHorizontal20Regular, FlipVertical20Regular, ZoomIn20Regular, ZoomOut20Regular, ArrowReset20Regular, ChevronDown16Regular, Grid20Regular, Crop20Regular, FullScreenMinimize20Regular, FullScreenMaximize20Regular, ArrowDownload20Regular, Prohibited20Regular, Checkmark20Filled, ArrowDownload24Regular, ArrowClockwise24Regular, ChevronUp12Regular, ChevronDown12Regular, DocumentArrowLeft24Regular, ChevronDown20Regular, ChevronRight20Regular, Folder20Regular, Document20Regular, ErrorCircle20Regular, Eye20Regular, Dismiss20Regular, Edit20Regular, Add16Regular, Edit16Regular, LinkAdd16Regular, LinkDismiss16Regular, Delete16Regular, Settings20Regular, Search20Regular, ArrowClockwise20Regular, ArrowSortDown16Regular, ArrowSortUp16Regular, Dismiss24Regular, LinkAdd20Regular, QuestionCircle24Regular, Info24Regular, DismissCircle24Regular, Warning24Regular, CheckmarkCircle24Regular, ArrowExit20Regular, PersonEdit20Regular, ArrowSwap20Regular, SignOut20Regular, MoreVerticalRegular, ArrowSyncRegular, ArrowDownloadRegular, Save24Regular, Delete24Regular, Add20Regular, Delete20Regular, LinkDismiss20Regular, ArrowUpload32Regular, ArrowUpload24Regular, ArrowSync24Regular, DrawerArrowDownload24Regular, Money20Regular, ErrorCircle12Filled, DocumentText20Regular, FolderZip20Regular, MusicNote220Regular, Video20Regular, Image20Regular, SlideText20Regular, DocumentTable20Regular, DocumentPdf20Regular, MoreHorizontal20Regular, ChevronDoubleLeft20Regular, ChevronLeft20Regular, ChevronDoubleRight20Regular, Info12Regular, Checkmark16Regular, Dismiss16Regular } from '@fluentui/react-icons';
5
+ import { ArrowRotateCounterclockwise20Regular, ArrowRotateClockwise20Regular, FlipHorizontal20Regular, FlipVertical20Regular, ZoomIn20Regular, ZoomOut20Regular, ArrowReset20Regular, ChevronDown16Regular, Grid20Regular, Crop20Regular, FullScreenMinimize20Regular, FullScreenMaximize20Regular, ArrowDownload20Regular, Prohibited20Regular, Checkmark20Filled, ArrowDownload24Regular, ArrowClockwise24Regular, ChevronUp12Regular, ChevronDown12Regular, DocumentArrowLeft24Regular, ChevronDown20Regular, ChevronRight20Regular, Folder20Regular, Document20Regular, ErrorCircle20Regular, Eye20Regular, Dismiss20Regular, Edit20Regular, Add16Regular, Edit16Regular, LinkAdd16Regular, LinkDismiss16Regular, Delete16Regular, Settings20Regular, Search20Regular, ArrowClockwise20Regular, ArrowSortDown16Regular, ArrowSortUp16Regular, Dismiss24Regular, LinkAdd20Regular, QuestionCircle24Regular, Info24Regular, DismissCircle24Regular, Warning24Regular, CheckmarkCircle24Regular, ArrowExit20Regular, PersonEdit20Regular, ArrowSwap20Regular, SignOut20Regular, MoreVerticalRegular, ArrowSyncRegular, EyeRegular, ArrowDownloadRegular, SearchRegular, DismissRegular, Save24Regular, Delete24Regular, Add20Regular, Delete20Regular, LinkDismiss20Regular, ArrowUpload32Regular, ArrowUpload24Regular, ArrowSync24Regular, DrawerArrowDownload24Regular, Money20Regular, ErrorCircle12Filled, DocumentText20Regular, FolderZip20Regular, MusicNote220Regular, Video20Regular, Image20Regular, SlideText20Regular, DocumentTable20Regular, DocumentPdf20Regular, MoreHorizontal20Regular, ChevronDoubleLeft20Regular, ChevronLeft20Regular, ChevronDoubleRight20Regular, Info12Regular, Checkmark16Regular, Dismiss16Regular } from '@fluentui/react-icons';
6
6
  import { RequiredLevel, DateTimeBehavior, getRecordReferenceId, RelationshipFilterMode, SwitchIdentityResult, TableSecurityPermission } from '@powerportalspro/core';
7
7
  import { DatePicker } from '@fluentui/react-datepicker-compat';
8
8
  import { TimePicker } from '@fluentui/react-timepicker-compat';
@@ -8304,6 +8304,36 @@ function MainGridVirtualizeFooter({
8304
8304
  ] });
8305
8305
  }
8306
8306
 
8307
+ // src/grids/virtualize-paging.ts
8308
+ function hasMoreRowsToLoad(state) {
8309
+ if (state.moreRecords === false) return false;
8310
+ if (state.moreRecords === true) return true;
8311
+ if (state.lastPageEmpty) return false;
8312
+ if (state.totalCount === void 0) return true;
8313
+ return state.accumulatedCount < state.totalCount;
8314
+ }
8315
+ var VirtualizePagingCookies = class {
8316
+ key;
8317
+ byPage = /* @__PURE__ */ new Map();
8318
+ /** Store the cookie the response for `page` came back with (no-op for a null / empty cookie). */
8319
+ record(key, page, cookie) {
8320
+ if (this.key !== key) {
8321
+ this.key = key;
8322
+ this.byPage = /* @__PURE__ */ new Map();
8323
+ }
8324
+ if (cookie) this.byPage.set(page, cookie);
8325
+ }
8326
+ /**
8327
+ * The cookie to send when requesting `page` under `key`: the one page
8328
+ * `page - 1` returned, or `undefined` when it isn't known (page 1, a
8329
+ * non-sequential jump, or a key the store has no cookies for).
8330
+ */
8331
+ cookieFor(key, page) {
8332
+ if (this.key !== key || page <= 1) return void 0;
8333
+ return this.byPage.get(page - 1);
8334
+ }
8335
+ };
8336
+
8307
8337
  // src/grids/persisted-grid-state.ts
8308
8338
  function readPersistedGridStateFromUrl(queryParameterName) {
8309
8339
  if (!queryParameterName) return null;
@@ -8835,8 +8865,22 @@ function MainGridImpl({
8835
8865
  `[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.`
8836
8866
  );
8837
8867
  }, [columnsKey, useInlineFetchXml]);
8868
+ const accumulatorResetKey = useMemo(
8869
+ () => [
8870
+ resolvedViewId ?? "",
8871
+ useInlineFetchXml ? inlineFetchXml ?? "" : "",
8872
+ debouncedSearchText,
8873
+ sort.map((s) => `${s.columnName}:${s.descending ? "d" : "a"}`).join("|"),
8874
+ filtersKey,
8875
+ pageSize,
8876
+ tableName ?? ""
8877
+ ].join("|"),
8878
+ [resolvedViewId, useInlineFetchXml, inlineFetchXml, debouncedSearchText, sort, filtersKey, pageSize, tableName]
8879
+ );
8880
+ const [virtualizeCookies] = useState(() => new VirtualizePagingCookies());
8838
8881
  const searchRequest = useMemo(() => {
8839
8882
  const trimmed = debouncedSearchText.trim();
8883
+ const pagingCookie = isVirtualize ? virtualizeCookies.cookieFor(accumulatorResetKey, page) : void 0;
8840
8884
  return {
8841
8885
  ...useInlineFetchXml && inlineFetchXml ? { fetchXml: inlineFetchXml } : resolvedViewId !== void 0 && { viewId: resolvedViewId },
8842
8886
  ...trimmed && { searchText: trimmed },
@@ -8856,7 +8900,8 @@ function MainGridImpl({
8856
8900
  },
8857
8901
  ...columnsOverride && { columns: [...columnsOverride] },
8858
8902
  pageNumber: page,
8859
- pageSize
8903
+ pageSize,
8904
+ ...pagingCookie && { pagingCookie }
8860
8905
  };
8861
8906
  }, [
8862
8907
  resolvedViewId,
@@ -8867,7 +8912,9 @@ function MainGridImpl({
8867
8912
  page,
8868
8913
  pageSize,
8869
8914
  filtersKey,
8870
- columnsKey
8915
+ columnsKey,
8916
+ isVirtualize,
8917
+ accumulatorResetKey
8871
8918
  ]);
8872
8919
  const transformPending = !!transformViewAsync || isCustomActiveView;
8873
8920
  const recordsResult = useGridData(searchRequest, {
@@ -9015,7 +9062,13 @@ function MainGridImpl({
9015
9062
  (row) => row.properties?.[columnName],
9016
9063
  {
9017
9064
  id: columnName,
9018
- header: () => resolvedColumn.displayName,
9065
+ // Headers normally show the server-resolved Dataverse display name, which bypasses the
9066
+ // client `t()` — so the ?ppp-show-keys aid couldn't reach them. Consult key-mode here too:
9067
+ // when it's on, show the column's localization key (the one a consumer would override),
9068
+ // keyed by the column's OWN owning table (`resolvedColumn.tableName` — the related table
9069
+ // for a linked-entity column, not the grid's primary table). For linked columns the name
9070
+ // is a dotted alias path, so take the segment after the last dot as the real column name.
9071
+ header: () => isKeyModeEnabled() ? `tables.${resolvedColumn.tableName}.columns.${columnName.split(".").pop() ?? columnName}.label` : resolvedColumn.displayName,
9019
9072
  cell: (cellCtx) => {
9020
9073
  const rowRecord = cellCtx.row.original;
9021
9074
  const rowId = rowRecord.id;
@@ -9158,18 +9211,6 @@ function MainGridImpl({
9158
9211
  ]);
9159
9212
  const pageRecords = dataSource ? dataSource.rows : recordsResult.data?.tableRecords ?? EMPTY_RECORDS;
9160
9213
  const [accumulatedRows, setAccumulatedRows] = useState(null);
9161
- const accumulatorResetKey = useMemo(
9162
- () => [
9163
- resolvedViewId ?? "",
9164
- useInlineFetchXml ? inlineFetchXml ?? "" : "",
9165
- debouncedSearchText,
9166
- sort.map((s) => `${s.columnName}:${s.descending ? "d" : "a"}`).join("|"),
9167
- filtersKey,
9168
- pageSize,
9169
- tableName ?? ""
9170
- ].join("|"),
9171
- [resolvedViewId, useInlineFetchXml, inlineFetchXml, debouncedSearchText, sort, filtersKey, pageSize, tableName]
9172
- );
9173
9214
  const resetVirtualizedView = useCallback(() => {
9174
9215
  setAccumulatedRows([]);
9175
9216
  setPage(1);
@@ -9195,6 +9236,12 @@ function MainGridImpl({
9195
9236
  return [...base, ...additions];
9196
9237
  });
9197
9238
  }, [isVirtualize, recordsResult.status, pageRecords]);
9239
+ useEffect(() => {
9240
+ if (!isVirtualize) return;
9241
+ const landed = recordsResult.data;
9242
+ if (!landed) return;
9243
+ virtualizeCookies.record(accumulatorResetKey, page, landed.pagingInfo?.pagingCookie);
9244
+ }, [isVirtualize, recordsResult.data]);
9198
9245
  const data = isVirtualize ? accumulatedRows ?? EMPTY_RECORDS : pageRecords;
9199
9246
  const [pendingRowUpdates, setPendingRowUpdates] = useState(() => /* @__PURE__ */ new Map());
9200
9247
  const [pendingRowCreates, setPendingRowCreates] = useState(() => []);
@@ -9675,7 +9722,15 @@ function MainGridImpl({
9675
9722
  if (raw == null) return void 0;
9676
9723
  return typeof raw === "number" ? raw : Number(raw);
9677
9724
  })();
9678
- const hasMoreRowsToLoad = isVirtualize && (totalCount === void 0 || (accumulatedRows?.length ?? 0) < totalCount);
9725
+ const hasMoreRowsToLoad2 = isVirtualize && hasMoreRowsToLoad({
9726
+ accumulatedCount: accumulatedRows?.length ?? 0,
9727
+ totalCount,
9728
+ moreRecords: dataSource ? dataSource.moreRecords : recordsResult.data?.pagingInfo?.moreRecords,
9729
+ // "A page has landed and it carried no rows." In standalone mode
9730
+ // `data` is only ever defined for a landed response (errors wipe it);
9731
+ // the datasource has no data handle, so idle-after-success stands in.
9732
+ lastPageEmpty: dataSource ? dataSource.status === QueryStatus.Success && !dataSource.isFetching && dataSource.rows.length === 0 : recordsResult.data !== void 0 && pageRecords.length === 0
9733
+ });
9679
9734
  useEffect(() => {
9680
9735
  if (!isVirtualize) return;
9681
9736
  const sentinel = loadMoreSentinelRef.current;
@@ -9686,7 +9741,7 @@ function MainGridImpl({
9686
9741
  const entry = entries[0];
9687
9742
  if (!entry?.isIntersecting) return;
9688
9743
  if (recordsResult.isFetching) return;
9689
- if (!hasMoreRowsToLoad) return;
9744
+ if (!hasMoreRowsToLoad2) return;
9690
9745
  setPage(page + 1);
9691
9746
  },
9692
9747
  {
@@ -9699,7 +9754,7 @@ function MainGridImpl({
9699
9754
  );
9700
9755
  observer.observe(sentinel);
9701
9756
  return () => observer.disconnect();
9702
- }, [isVirtualize, hasMoreRowsToLoad, recordsResult.isFetching, page, setPage]);
9757
+ }, [isVirtualize, hasMoreRowsToLoad2, recordsResult.isFetching, page, setPage]);
9703
9758
  const handlePageChange = (nextPage) => {
9704
9759
  if (nextPage === page) return;
9705
9760
  setPage(nextPage);
@@ -10319,7 +10374,7 @@ function MainGridImpl({
10319
10374
  }
10320
10375
  ),
10321
10376
  displayedData.length === 0 && /* @__PURE__ */ jsx("div", { className: styles.emptyStateRow, children: t("app.components.grid.empty-content") }),
10322
- isVirtualize && hasMoreRowsToLoad && // No spinner here: the full-body `refetchScrim` overlay below is the
10377
+ isVirtualize && hasMoreRowsToLoad2 && // No spinner here: the full-body `refetchScrim` overlay below is the
10323
10378
  // single load indicator. Rendering a `size="tiny"` spinner in the
10324
10379
  // sentinel as well meant a deps-change refetch (refresh / sort /
10325
10380
  // search / view switch) — which resets the accumulator to empty and
@@ -13558,27 +13613,57 @@ function NavMenu({ children, width, className, style }) {
13558
13613
  useNavSelectedStyles();
13559
13614
  const location = useLocation();
13560
13615
  const navigate = useNavigate();
13561
- const { allRoutes, defaultOpen } = useMemo(
13616
+ const { allRoutes, groups, defaultOpenIndexes } = useMemo(
13562
13617
  () => collectNavTree(children),
13563
13618
  [children]
13564
13619
  );
13565
- const selectedValue = useMemo(
13620
+ const selectedRoute = useMemo(
13566
13621
  () => bestRouteMatch(allRoutes, location.pathname),
13567
13622
  [allRoutes, location.pathname]
13568
13623
  );
13624
+ const [openIndexes, setOpenIndexes] = useState(
13625
+ () => /* @__PURE__ */ new Set([
13626
+ ...defaultOpenIndexes,
13627
+ // Seed the active route's group too, so it's open on first paint
13628
+ // (the effect below only fires after mount).
13629
+ ...selectedRoute && selectedRoute.groupIndex >= 0 ? [selectedRoute.groupIndex] : []
13630
+ ])
13631
+ );
13632
+ const selectedGroupIndex = selectedRoute?.groupIndex ?? -1;
13633
+ useEffect(() => {
13634
+ if (selectedGroupIndex < 0) return;
13635
+ setOpenIndexes(
13636
+ (prev) => prev.has(selectedGroupIndex) ? prev : new Set(prev).add(selectedGroupIndex)
13637
+ );
13638
+ }, [selectedGroupIndex]);
13639
+ const openCategories = useMemo(
13640
+ () => groups.filter((_, index) => openIndexes.has(index)),
13641
+ [groups, openIndexes]
13642
+ );
13569
13643
  const handleSelect = (_, data) => {
13570
13644
  const value = data.value;
13571
13645
  if (typeof value === "string" && value.startsWith("/")) {
13572
13646
  navigate(value);
13573
13647
  }
13574
13648
  };
13649
+ const handleCategoryToggle = (_, data) => {
13650
+ const index = groups.indexOf(data.value);
13651
+ if (index < 0) return;
13652
+ setOpenIndexes((prev) => {
13653
+ const next = new Set(prev);
13654
+ if (next.has(index)) next.delete(index);
13655
+ else next.add(index);
13656
+ return next;
13657
+ });
13658
+ };
13575
13659
  const navStyle = width ? { ...style, minWidth: width, maxWidth: width } : style;
13576
13660
  return /* @__PURE__ */ jsx(
13577
13661
  Nav,
13578
13662
  {
13579
- selectedValue: selectedValue ?? "",
13663
+ selectedValue: selectedRoute?.to ?? "",
13580
13664
  onNavItemSelect: handleSelect,
13581
- defaultOpenCategories: defaultOpen,
13665
+ openCategories,
13666
+ onNavCategoryItemToggle: handleCategoryToggle,
13582
13667
  className: mergeClasses("ppp-nav-menu", styles.nav, className),
13583
13668
  style: navStyle,
13584
13669
  children
@@ -13608,42 +13693,46 @@ function NavItem({ to, icon, children }) {
13608
13693
  }
13609
13694
  function collectNavTree(children) {
13610
13695
  const allRoutes = [];
13611
- const defaultOpen = [];
13612
- const visit = (nodes) => {
13696
+ const groups = [];
13697
+ const defaultOpenIndexes = [];
13698
+ const visit = (nodes, groupIndex) => {
13613
13699
  Children.forEach(nodes, (child) => {
13614
13700
  if (!isValidElement(child)) return;
13615
13701
  if (child.type === NavGroup) {
13616
13702
  const props = child.props;
13617
13703
  const value = props.value ?? `group:${props.label}`;
13618
- if (props.defaultExpanded) defaultOpen.push(value);
13619
- visit(props.children);
13704
+ const index = groups.length;
13705
+ groups.push(value);
13706
+ if (props.defaultExpanded) defaultOpenIndexes.push(index);
13707
+ visit(props.children, index);
13620
13708
  return;
13621
13709
  }
13622
13710
  if (child.type === NavItem) {
13623
13711
  const props = child.props;
13624
13712
  allRoutes.push({
13625
13713
  to: props.to,
13626
- match: props.match ?? (props.to === "/" ? "exact" : "prefix")
13714
+ match: props.match ?? (props.to === "/" ? "exact" : "prefix"),
13715
+ groupIndex
13627
13716
  });
13628
13717
  return;
13629
13718
  }
13630
13719
  const wrapperChildren = child.props.children;
13631
- if (wrapperChildren !== void 0) visit(wrapperChildren);
13720
+ if (wrapperChildren !== void 0) visit(wrapperChildren, groupIndex);
13632
13721
  });
13633
13722
  };
13634
- visit(children);
13635
- return { allRoutes, defaultOpen };
13723
+ visit(children, -1);
13724
+ return { allRoutes, groups, defaultOpenIndexes };
13636
13725
  }
13637
13726
  function bestRouteMatch(routes, pathname) {
13638
13727
  const normalize = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
13639
13728
  const target = normalize(pathname);
13640
13729
  let best;
13641
13730
  let bestLen = -1;
13642
- for (const { to, match } of routes) {
13643
- const route = normalize(to);
13644
- const matches = route === target || match === "prefix" && (route === "/" || target.startsWith(route + "/"));
13731
+ for (const candidate of routes) {
13732
+ const route = normalize(candidate.to);
13733
+ const matches = route === target || candidate.match === "prefix" && (route === "/" || target.startsWith(route + "/"));
13645
13734
  if (matches && route.length > bestLen) {
13646
- best = to;
13735
+ best = candidate;
13647
13736
  bestLen = route.length;
13648
13737
  }
13649
13738
  }
@@ -14386,8 +14475,40 @@ var useStyles12 = makeStyles({
14386
14475
  mergedButtons: {
14387
14476
  display: "flex",
14388
14477
  flexWrap: "wrap",
14389
- gap: tokens.spacingHorizontalS
14478
+ gap: tokens.spacingHorizontalL
14479
+ },
14480
+ // One culture group: language label + inspect button + download button, kept together.
14481
+ mergedCulture: {
14482
+ display: "flex",
14483
+ alignItems: "center",
14484
+ gap: tokens.spacingHorizontalXS
14485
+ },
14486
+ mergedCultureLabel: { fontSize: tokens.fontSizeBase200 },
14487
+ dialogSurface: { maxWidth: "1100px", width: "90vw" },
14488
+ dialogToolbar: {
14489
+ display: "flex",
14490
+ alignItems: "center",
14491
+ gap: tokens.spacingHorizontalM,
14492
+ flexWrap: "wrap",
14493
+ marginBottom: tokens.spacingVerticalS
14494
+ },
14495
+ // Scroll viewport for the merged grid so a tall result set scrolls inside the dialog.
14496
+ dialogGrid: { maxHeight: "60vh", overflowY: "auto", overflowX: "auto" },
14497
+ // Merged-snapshot cells. Keys and sources are long single tokens (dotted keys,
14498
+ // backslash-only absolute paths) with no natural break opportunity, so without
14499
+ // this the text paints straight over the neighbouring column. `overflow-wrap:
14500
+ // anywhere` (not just `break-word`) is what makes the flex cell honour its
14501
+ // column width — it counts the soft breaks toward min-content — so the token
14502
+ // wraps inside the column instead of stretching it. Same recipe as
14503
+ // `sourceCell` in the per-source loads table above.
14504
+ mergedCell: {
14505
+ whiteSpace: "normal",
14506
+ overflowWrap: "anywhere",
14507
+ wordBreak: "break-word"
14390
14508
  },
14509
+ filterInput: { minWidth: "260px" },
14510
+ pager: { display: "flex", alignItems: "center", gap: tokens.spacingHorizontalS },
14511
+ grow: { flex: 1 },
14391
14512
  sectionBody: {
14392
14513
  display: "flex",
14393
14514
  flexDirection: "column",
@@ -14406,6 +14527,15 @@ function downloadBytes(fileName, data, contentType = "application/json") {
14406
14527
  anchor.click();
14407
14528
  URL.revokeObjectURL(url);
14408
14529
  }
14530
+ var kindKeyOf = (kind) => KIND_KEY[Number(kind)] ?? "tablemetadata";
14531
+ function cultureLabel(culture) {
14532
+ try {
14533
+ const name = new Intl.DisplayNames(void 0, { type: "language" }).of(culture);
14534
+ return name && name.toLowerCase() !== culture.toLowerCase() ? `${name} (${culture})` : culture;
14535
+ } catch {
14536
+ return culture;
14537
+ }
14538
+ }
14409
14539
  function LocalizationAdmin({ className }) {
14410
14540
  const styles = useStyles12();
14411
14541
  const t = useT();
@@ -14415,6 +14545,7 @@ function LocalizationAdmin({ className }) {
14415
14545
  const [isReloading, setIsReloading] = useState(false);
14416
14546
  const [isDownloading, setIsDownloading] = useState(false);
14417
14547
  const [message, setMessage] = useState();
14548
+ const [mergedDialogCulture, setMergedDialogCulture] = useState(null);
14418
14549
  const loadOverview = useCallback(async () => {
14419
14550
  try {
14420
14551
  const value = await ppp.getLocalizationOverviewAsync();
@@ -14572,17 +14703,44 @@ function LocalizationAdmin({ className }) {
14572
14703
  /* @__PURE__ */ jsx(LocalizationTranslator, {}),
14573
14704
  availableCultures.length > 0 && /* @__PURE__ */ jsx(Section, { headerText: t("app.localization-admin.download-merged-title"), borderVisible: true, children: /* @__PURE__ */ jsx(SectionColumn, { children: /* @__PURE__ */ jsxs("div", { className: styles.sectionBody, children: [
14574
14705
  /* @__PURE__ */ jsx(Text, { children: t("app.localization-admin.download-merged-description") }),
14575
- /* @__PURE__ */ jsx("div", { className: styles.mergedButtons, children: availableCultures.map((culture) => /* @__PURE__ */ jsx(
14576
- Button,
14577
- {
14578
- appearance: "outline",
14579
- disabled: isDownloading,
14580
- icon: /* @__PURE__ */ jsx(ArrowDownloadRegular, {}),
14581
- onClick: () => onDownloadMerged(culture),
14582
- children: culture
14583
- },
14584
- culture
14585
- )) })
14706
+ /* @__PURE__ */ jsx("div", { className: styles.mergedButtons, children: availableCultures.map((culture) => /* @__PURE__ */ jsxs("div", { className: styles.mergedCulture, children: [
14707
+ /* @__PURE__ */ jsx("span", { className: styles.mergedCultureLabel, children: cultureLabel(culture) }),
14708
+ /* @__PURE__ */ jsx(
14709
+ Tooltip,
14710
+ {
14711
+ content: t("app.localization-admin.view-merged-tooltip", [
14712
+ cultureLabel(culture)
14713
+ ]),
14714
+ relationship: "label",
14715
+ children: /* @__PURE__ */ jsx(
14716
+ Button,
14717
+ {
14718
+ appearance: "outline",
14719
+ icon: /* @__PURE__ */ jsx(EyeRegular, {}),
14720
+ onClick: () => setMergedDialogCulture(culture)
14721
+ }
14722
+ )
14723
+ }
14724
+ ),
14725
+ /* @__PURE__ */ jsx(
14726
+ Tooltip,
14727
+ {
14728
+ content: t("app.localization-admin.download-merged-tooltip", [
14729
+ cultureLabel(culture)
14730
+ ]),
14731
+ relationship: "label",
14732
+ children: /* @__PURE__ */ jsx(
14733
+ Button,
14734
+ {
14735
+ appearance: "subtle",
14736
+ disabled: isDownloading,
14737
+ icon: /* @__PURE__ */ jsx(ArrowDownloadRegular, {}),
14738
+ onClick: () => onDownloadMerged(culture)
14739
+ }
14740
+ )
14741
+ }
14742
+ )
14743
+ ] }, culture)) })
14586
14744
  ] }) }) }),
14587
14745
  /* @__PURE__ */ jsx(Section, { headerText: t("app.localization-admin.sources-title"), borderVisible: true, children: /* @__PURE__ */ jsx(SectionColumn, { children: /* @__PURE__ */ jsxs("div", { className: styles.sectionBody, children: [
14588
14746
  /* @__PURE__ */ jsx(Text, { children: t("app.localization-admin.sources-description") }),
@@ -14613,7 +14771,22 @@ function LocalizationAdmin({ className }) {
14613
14771
  ] }) }) })
14614
14772
  ] });
14615
14773
  }
14616
- return /* @__PURE__ */ jsx("div", { className: mergeClasses("ppp-localization-admin", styles.root, className), children: content });
14774
+ return /* @__PURE__ */ jsxs("div", { className: mergeClasses("ppp-localization-admin", styles.root, className), children: [
14775
+ content,
14776
+ mergedDialogCulture !== null && /* @__PURE__ */ jsx(
14777
+ MergedSourcesDialog,
14778
+ {
14779
+ culture: mergedDialogCulture,
14780
+ label: cultureLabel(mergedDialogCulture),
14781
+ onClose: () => setMergedDialogCulture(null),
14782
+ onDownload: () => onDownloadMerged(mergedDialogCulture),
14783
+ isDownloading,
14784
+ ppp,
14785
+ t,
14786
+ styles
14787
+ }
14788
+ )
14789
+ ] });
14617
14790
  }
14618
14791
  function ConfigRow({ styles, label, value }) {
14619
14792
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -14663,6 +14836,177 @@ function SourceRow({
14663
14836
  )) }) })
14664
14837
  ] });
14665
14838
  }
14839
+ var MERGED_PAGE_SIZE = 100;
14840
+ function MergedSourcesDialog({
14841
+ culture,
14842
+ label,
14843
+ onClose,
14844
+ onDownload,
14845
+ isDownloading,
14846
+ ppp,
14847
+ t,
14848
+ styles
14849
+ }) {
14850
+ const [entries, setEntries] = useState(null);
14851
+ const [loadError, setLoadError] = useState();
14852
+ const [filter, setFilter] = useState("");
14853
+ const [page, setPage] = useState(0);
14854
+ const [sortState, setSortState] = useState({
14855
+ sortColumn: "key",
14856
+ sortDirection: "ascending"
14857
+ });
14858
+ useEffect(() => {
14859
+ let cancelled = false;
14860
+ void (async () => {
14861
+ try {
14862
+ const value = await ppp.getMergedLocalizationEntriesAsync(culture);
14863
+ if (!cancelled) {
14864
+ setEntries(value);
14865
+ setLoadError(void 0);
14866
+ }
14867
+ } catch (reason) {
14868
+ if (!cancelled) {
14869
+ setEntries(null);
14870
+ setLoadError(reason instanceof Error ? reason.message : String(reason));
14871
+ }
14872
+ }
14873
+ })();
14874
+ return () => {
14875
+ cancelled = true;
14876
+ };
14877
+ }, [ppp, culture]);
14878
+ const columns = useMemo(
14879
+ () => [
14880
+ createTableColumn({
14881
+ columnId: "key",
14882
+ compare: (a, b) => a.key.localeCompare(b.key),
14883
+ renderHeaderCell: () => t("app.localization-admin.merged-column-key"),
14884
+ renderCell: (item) => item.key
14885
+ }),
14886
+ createTableColumn({
14887
+ columnId: "kind",
14888
+ compare: (a, b) => Number(a.sourceKind) - Number(b.sourceKind),
14889
+ renderHeaderCell: () => t("app.localization-admin.merged-column-kind"),
14890
+ renderCell: (item) => t(`app.localization-admin.kind-${kindKeyOf(item.sourceKind)}`)
14891
+ }),
14892
+ createTableColumn({
14893
+ columnId: "source",
14894
+ compare: (a, b) => a.sourceDisplayName.localeCompare(b.sourceDisplayName),
14895
+ renderHeaderCell: () => t("app.localization-admin.merged-column-source"),
14896
+ renderCell: (item) => item.sourceDisplayName
14897
+ }),
14898
+ createTableColumn({
14899
+ columnId: "value",
14900
+ compare: (a, b) => a.value.localeCompare(b.value),
14901
+ renderHeaderCell: () => t("app.localization-admin.merged-column-value"),
14902
+ renderCell: (item) => item.value
14903
+ })
14904
+ ],
14905
+ [t]
14906
+ );
14907
+ const filtered = useMemo(() => {
14908
+ if (!entries) return [];
14909
+ const q = filter.trim().toLowerCase();
14910
+ const base = q ? entries.filter(
14911
+ (e) => e.key.toLowerCase().includes(q) || e.value.toLowerCase().includes(q) || e.sourceDisplayName.toLowerCase().includes(q)
14912
+ ) : entries;
14913
+ return [...base];
14914
+ }, [entries, filter]);
14915
+ const sorted = useMemo(() => {
14916
+ const column = columns.find((c) => c.columnId === sortState.sortColumn);
14917
+ if (!column?.compare) return filtered;
14918
+ const sign = sortState.sortDirection === "ascending" ? 1 : -1;
14919
+ return [...filtered].sort((a, b) => sign * column.compare(a, b));
14920
+ }, [filtered, columns, sortState]);
14921
+ useEffect(() => {
14922
+ setPage(0);
14923
+ }, [filter, sortState]);
14924
+ const pageCount = Math.max(1, Math.ceil(sorted.length / MERGED_PAGE_SIZE));
14925
+ const currentPage = Math.min(page, pageCount - 1);
14926
+ const paged = sorted.slice(currentPage * MERGED_PAGE_SIZE, currentPage * MERGED_PAGE_SIZE + MERGED_PAGE_SIZE);
14927
+ const columnSizingOptions = useMemo(
14928
+ () => ({
14929
+ key: { minWidth: 200, defaultWidth: 320 },
14930
+ kind: { minWidth: 110, defaultWidth: 140 },
14931
+ source: { minWidth: 200, defaultWidth: 300 },
14932
+ value: { minWidth: 200, defaultWidth: 360 }
14933
+ }),
14934
+ []
14935
+ );
14936
+ return /* @__PURE__ */ jsx(Dialog, { open: true, onOpenChange: (_event, data) => !data.open && onClose(), children: /* @__PURE__ */ jsx(DialogSurface, { className: styles.dialogSurface, children: /* @__PURE__ */ jsxs(DialogBody, { children: [
14937
+ /* @__PURE__ */ jsx(DialogTitle, { children: t("app.localization-admin.merged-dialog-title", [label]) }),
14938
+ /* @__PURE__ */ jsx(DialogContent, { children: loadError !== void 0 ? /* @__PURE__ */ jsx(MessageBar, { intent: "error", children: /* @__PURE__ */ jsx(MessageBarBody, { children: t("app.localization-admin.merged-load-failed", [loadError]) }) }) : entries === null ? /* @__PURE__ */ jsx(Spinner, {}) : /* @__PURE__ */ jsxs(Fragment, { children: [
14939
+ /* @__PURE__ */ jsxs("div", { className: styles.dialogToolbar, children: [
14940
+ /* @__PURE__ */ jsx(
14941
+ Input,
14942
+ {
14943
+ className: styles.filterInput,
14944
+ value: filter,
14945
+ onChange: (_event, data) => setFilter(data.value),
14946
+ contentBefore: /* @__PURE__ */ jsx(SearchRegular, {}),
14947
+ placeholder: t("app.localization-admin.merged-filter-placeholder")
14948
+ }
14949
+ ),
14950
+ /* @__PURE__ */ jsx("span", { className: styles.lastReloaded, children: t("app.localization-admin.merged-count", [sorted.length, entries.length]) }),
14951
+ /* @__PURE__ */ jsx("span", { className: styles.grow }),
14952
+ pageCount > 1 && /* @__PURE__ */ jsxs("span", { className: styles.pager, children: [
14953
+ /* @__PURE__ */ jsx(
14954
+ Button,
14955
+ {
14956
+ size: "small",
14957
+ appearance: "subtle",
14958
+ disabled: currentPage === 0,
14959
+ onClick: () => setPage(currentPage - 1),
14960
+ children: t("app.localization-admin.merged-prev")
14961
+ }
14962
+ ),
14963
+ /* @__PURE__ */ jsx("span", { className: styles.lastReloaded, children: t("app.localization-admin.merged-page", [currentPage + 1, pageCount]) }),
14964
+ /* @__PURE__ */ jsx(
14965
+ Button,
14966
+ {
14967
+ size: "small",
14968
+ appearance: "subtle",
14969
+ disabled: currentPage >= pageCount - 1,
14970
+ onClick: () => setPage(currentPage + 1),
14971
+ children: t("app.localization-admin.merged-next")
14972
+ }
14973
+ )
14974
+ ] })
14975
+ ] }),
14976
+ sorted.length === 0 ? /* @__PURE__ */ jsx(MessageBar, { intent: "info", children: /* @__PURE__ */ jsx(MessageBarBody, { children: t("app.localization-admin.merged-empty") }) }) : /* @__PURE__ */ jsx("div", { className: styles.dialogGrid, children: /* @__PURE__ */ jsxs(
14977
+ DataGrid,
14978
+ {
14979
+ items: paged,
14980
+ columns,
14981
+ sortable: true,
14982
+ resizableColumns: true,
14983
+ sortState,
14984
+ onSortChange: (_event, next) => setSortState(next),
14985
+ columnSizingOptions,
14986
+ getRowId: (item) => item.key,
14987
+ size: "small",
14988
+ children: [
14989
+ /* @__PURE__ */ jsx(DataGridHeader, { children: /* @__PURE__ */ jsx(DataGridRow, { children: ({ renderHeaderCell }) => /* @__PURE__ */ jsx(DataGridHeaderCell, { children: renderHeaderCell() }) }) }),
14990
+ /* @__PURE__ */ jsx(DataGridBody, { children: ({ item, rowId }) => /* @__PURE__ */ jsx(DataGridRow, { children: ({ renderCell }) => /* @__PURE__ */ jsx(DataGridCell, { className: styles.mergedCell, children: renderCell(item) }) }, rowId) })
14991
+ ]
14992
+ }
14993
+ ) })
14994
+ ] }) }),
14995
+ /* @__PURE__ */ jsxs(DialogActions, { children: [
14996
+ /* @__PURE__ */ jsx(
14997
+ Button,
14998
+ {
14999
+ appearance: "primary",
15000
+ icon: /* @__PURE__ */ jsx(ArrowDownloadRegular, {}),
15001
+ disabled: isDownloading,
15002
+ onClick: onDownload,
15003
+ children: t("app.localization-admin.merged-download-button")
15004
+ }
15005
+ ),
15006
+ /* @__PURE__ */ jsx(Button, { appearance: "secondary", icon: /* @__PURE__ */ jsx(DismissRegular, {}), onClick: onClose, children: t("app.buttons.close.label") })
15007
+ ] })
15008
+ ] }) }) });
15009
+ }
14666
15010
  var useStyles13 = makeStyles({
14667
15011
  root: {
14668
15012
  display: "flex",