@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.cjs CHANGED
@@ -8319,6 +8319,36 @@ function MainGridVirtualizeFooter({
8319
8319
  ] });
8320
8320
  }
8321
8321
 
8322
+ // src/grids/virtualize-paging.ts
8323
+ function hasMoreRowsToLoad(state) {
8324
+ if (state.moreRecords === false) return false;
8325
+ if (state.moreRecords === true) return true;
8326
+ if (state.lastPageEmpty) return false;
8327
+ if (state.totalCount === void 0) return true;
8328
+ return state.accumulatedCount < state.totalCount;
8329
+ }
8330
+ var VirtualizePagingCookies = class {
8331
+ key;
8332
+ byPage = /* @__PURE__ */ new Map();
8333
+ /** Store the cookie the response for `page` came back with (no-op for a null / empty cookie). */
8334
+ record(key, page, cookie) {
8335
+ if (this.key !== key) {
8336
+ this.key = key;
8337
+ this.byPage = /* @__PURE__ */ new Map();
8338
+ }
8339
+ if (cookie) this.byPage.set(page, cookie);
8340
+ }
8341
+ /**
8342
+ * The cookie to send when requesting `page` under `key`: the one page
8343
+ * `page - 1` returned, or `undefined` when it isn't known (page 1, a
8344
+ * non-sequential jump, or a key the store has no cookies for).
8345
+ */
8346
+ cookieFor(key, page) {
8347
+ if (this.key !== key || page <= 1) return void 0;
8348
+ return this.byPage.get(page - 1);
8349
+ }
8350
+ };
8351
+
8322
8352
  // src/grids/persisted-grid-state.ts
8323
8353
  function readPersistedGridStateFromUrl(queryParameterName) {
8324
8354
  if (!queryParameterName) return null;
@@ -8850,8 +8880,22 @@ function MainGridImpl({
8850
8880
  `[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
8881
  );
8852
8882
  }, [columnsKey, useInlineFetchXml]);
8883
+ const accumulatorResetKey = react.useMemo(
8884
+ () => [
8885
+ resolvedViewId ?? "",
8886
+ useInlineFetchXml ? inlineFetchXml ?? "" : "",
8887
+ debouncedSearchText,
8888
+ sort.map((s) => `${s.columnName}:${s.descending ? "d" : "a"}`).join("|"),
8889
+ filtersKey,
8890
+ pageSize,
8891
+ tableName ?? ""
8892
+ ].join("|"),
8893
+ [resolvedViewId, useInlineFetchXml, inlineFetchXml, debouncedSearchText, sort, filtersKey, pageSize, tableName]
8894
+ );
8895
+ const [virtualizeCookies] = react.useState(() => new VirtualizePagingCookies());
8853
8896
  const searchRequest = react.useMemo(() => {
8854
8897
  const trimmed = debouncedSearchText.trim();
8898
+ const pagingCookie = isVirtualize ? virtualizeCookies.cookieFor(accumulatorResetKey, page) : void 0;
8855
8899
  return {
8856
8900
  ...useInlineFetchXml && inlineFetchXml ? { fetchXml: inlineFetchXml } : resolvedViewId !== void 0 && { viewId: resolvedViewId },
8857
8901
  ...trimmed && { searchText: trimmed },
@@ -8871,7 +8915,8 @@ function MainGridImpl({
8871
8915
  },
8872
8916
  ...columnsOverride && { columns: [...columnsOverride] },
8873
8917
  pageNumber: page,
8874
- pageSize
8918
+ pageSize,
8919
+ ...pagingCookie && { pagingCookie }
8875
8920
  };
8876
8921
  }, [
8877
8922
  resolvedViewId,
@@ -8882,7 +8927,9 @@ function MainGridImpl({
8882
8927
  page,
8883
8928
  pageSize,
8884
8929
  filtersKey,
8885
- columnsKey
8930
+ columnsKey,
8931
+ isVirtualize,
8932
+ accumulatorResetKey
8886
8933
  ]);
8887
8934
  const transformPending = !!transformViewAsync || isCustomActiveView;
8888
8935
  const recordsResult = react$1.useGridData(searchRequest, {
@@ -9030,7 +9077,13 @@ function MainGridImpl({
9030
9077
  (row) => row.properties?.[columnName],
9031
9078
  {
9032
9079
  id: columnName,
9033
- header: () => resolvedColumn.displayName,
9080
+ // Headers normally show the server-resolved Dataverse display name, which bypasses the
9081
+ // client `t()` — so the ?ppp-show-keys aid couldn't reach them. Consult key-mode here too:
9082
+ // when it's on, show the column's localization key (the one a consumer would override),
9083
+ // keyed by the column's OWN owning table (`resolvedColumn.tableName` — the related table
9084
+ // for a linked-entity column, not the grid's primary table). For linked columns the name
9085
+ // is a dotted alias path, so take the segment after the last dot as the real column name.
9086
+ header: () => react$1.isKeyModeEnabled() ? `tables.${resolvedColumn.tableName}.columns.${columnName.split(".").pop() ?? columnName}.label` : resolvedColumn.displayName,
9034
9087
  cell: (cellCtx) => {
9035
9088
  const rowRecord = cellCtx.row.original;
9036
9089
  const rowId = rowRecord.id;
@@ -9173,18 +9226,6 @@ function MainGridImpl({
9173
9226
  ]);
9174
9227
  const pageRecords = dataSource ? dataSource.rows : recordsResult.data?.tableRecords ?? EMPTY_RECORDS;
9175
9228
  const [accumulatedRows, setAccumulatedRows] = react.useState(null);
9176
- const accumulatorResetKey = react.useMemo(
9177
- () => [
9178
- resolvedViewId ?? "",
9179
- useInlineFetchXml ? inlineFetchXml ?? "" : "",
9180
- debouncedSearchText,
9181
- sort.map((s) => `${s.columnName}:${s.descending ? "d" : "a"}`).join("|"),
9182
- filtersKey,
9183
- pageSize,
9184
- tableName ?? ""
9185
- ].join("|"),
9186
- [resolvedViewId, useInlineFetchXml, inlineFetchXml, debouncedSearchText, sort, filtersKey, pageSize, tableName]
9187
- );
9188
9229
  const resetVirtualizedView = react.useCallback(() => {
9189
9230
  setAccumulatedRows([]);
9190
9231
  setPage(1);
@@ -9210,6 +9251,12 @@ function MainGridImpl({
9210
9251
  return [...base, ...additions];
9211
9252
  });
9212
9253
  }, [isVirtualize, recordsResult.status, pageRecords]);
9254
+ react.useEffect(() => {
9255
+ if (!isVirtualize) return;
9256
+ const landed = recordsResult.data;
9257
+ if (!landed) return;
9258
+ virtualizeCookies.record(accumulatorResetKey, page, landed.pagingInfo?.pagingCookie);
9259
+ }, [isVirtualize, recordsResult.data]);
9213
9260
  const data = isVirtualize ? accumulatedRows ?? EMPTY_RECORDS : pageRecords;
9214
9261
  const [pendingRowUpdates, setPendingRowUpdates] = react.useState(() => /* @__PURE__ */ new Map());
9215
9262
  const [pendingRowCreates, setPendingRowCreates] = react.useState(() => []);
@@ -9690,7 +9737,15 @@ function MainGridImpl({
9690
9737
  if (raw == null) return void 0;
9691
9738
  return typeof raw === "number" ? raw : Number(raw);
9692
9739
  })();
9693
- const hasMoreRowsToLoad = isVirtualize && (totalCount === void 0 || (accumulatedRows?.length ?? 0) < totalCount);
9740
+ const hasMoreRowsToLoad2 = isVirtualize && hasMoreRowsToLoad({
9741
+ accumulatedCount: accumulatedRows?.length ?? 0,
9742
+ totalCount,
9743
+ moreRecords: dataSource ? dataSource.moreRecords : recordsResult.data?.pagingInfo?.moreRecords,
9744
+ // "A page has landed and it carried no rows." In standalone mode
9745
+ // `data` is only ever defined for a landed response (errors wipe it);
9746
+ // the datasource has no data handle, so idle-after-success stands in.
9747
+ lastPageEmpty: dataSource ? dataSource.status === react$1.QueryStatus.Success && !dataSource.isFetching && dataSource.rows.length === 0 : recordsResult.data !== void 0 && pageRecords.length === 0
9748
+ });
9694
9749
  react.useEffect(() => {
9695
9750
  if (!isVirtualize) return;
9696
9751
  const sentinel = loadMoreSentinelRef.current;
@@ -9701,7 +9756,7 @@ function MainGridImpl({
9701
9756
  const entry = entries[0];
9702
9757
  if (!entry?.isIntersecting) return;
9703
9758
  if (recordsResult.isFetching) return;
9704
- if (!hasMoreRowsToLoad) return;
9759
+ if (!hasMoreRowsToLoad2) return;
9705
9760
  setPage(page + 1);
9706
9761
  },
9707
9762
  {
@@ -9714,7 +9769,7 @@ function MainGridImpl({
9714
9769
  );
9715
9770
  observer.observe(sentinel);
9716
9771
  return () => observer.disconnect();
9717
- }, [isVirtualize, hasMoreRowsToLoad, recordsResult.isFetching, page, setPage]);
9772
+ }, [isVirtualize, hasMoreRowsToLoad2, recordsResult.isFetching, page, setPage]);
9718
9773
  const handlePageChange = (nextPage) => {
9719
9774
  if (nextPage === page) return;
9720
9775
  setPage(nextPage);
@@ -10334,7 +10389,7 @@ function MainGridImpl({
10334
10389
  }
10335
10390
  ),
10336
10391
  displayedData.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.emptyStateRow, children: t("app.components.grid.empty-content") }),
10337
- isVirtualize && hasMoreRowsToLoad && // No spinner here: the full-body `refetchScrim` overlay below is the
10392
+ isVirtualize && hasMoreRowsToLoad2 && // No spinner here: the full-body `refetchScrim` overlay below is the
10338
10393
  // single load indicator. Rendering a `size="tiny"` spinner in the
10339
10394
  // sentinel as well meant a deps-change refetch (refresh / sort /
10340
10395
  // search / view switch) — which resets the accumulator to empty and
@@ -13573,27 +13628,57 @@ function NavMenu({ children, width, className, style }) {
13573
13628
  useNavSelectedStyles();
13574
13629
  const location = reactRouterDom.useLocation();
13575
13630
  const navigate = reactRouterDom.useNavigate();
13576
- const { allRoutes, defaultOpen } = react.useMemo(
13631
+ const { allRoutes, groups, defaultOpenIndexes } = react.useMemo(
13577
13632
  () => collectNavTree(children),
13578
13633
  [children]
13579
13634
  );
13580
- const selectedValue = react.useMemo(
13635
+ const selectedRoute = react.useMemo(
13581
13636
  () => bestRouteMatch(allRoutes, location.pathname),
13582
13637
  [allRoutes, location.pathname]
13583
13638
  );
13639
+ const [openIndexes, setOpenIndexes] = react.useState(
13640
+ () => /* @__PURE__ */ new Set([
13641
+ ...defaultOpenIndexes,
13642
+ // Seed the active route's group too, so it's open on first paint
13643
+ // (the effect below only fires after mount).
13644
+ ...selectedRoute && selectedRoute.groupIndex >= 0 ? [selectedRoute.groupIndex] : []
13645
+ ])
13646
+ );
13647
+ const selectedGroupIndex = selectedRoute?.groupIndex ?? -1;
13648
+ react.useEffect(() => {
13649
+ if (selectedGroupIndex < 0) return;
13650
+ setOpenIndexes(
13651
+ (prev) => prev.has(selectedGroupIndex) ? prev : new Set(prev).add(selectedGroupIndex)
13652
+ );
13653
+ }, [selectedGroupIndex]);
13654
+ const openCategories = react.useMemo(
13655
+ () => groups.filter((_, index) => openIndexes.has(index)),
13656
+ [groups, openIndexes]
13657
+ );
13584
13658
  const handleSelect = (_, data) => {
13585
13659
  const value = data.value;
13586
13660
  if (typeof value === "string" && value.startsWith("/")) {
13587
13661
  navigate(value);
13588
13662
  }
13589
13663
  };
13664
+ const handleCategoryToggle = (_, data) => {
13665
+ const index = groups.indexOf(data.value);
13666
+ if (index < 0) return;
13667
+ setOpenIndexes((prev) => {
13668
+ const next = new Set(prev);
13669
+ if (next.has(index)) next.delete(index);
13670
+ else next.add(index);
13671
+ return next;
13672
+ });
13673
+ };
13590
13674
  const navStyle = width ? { ...style, minWidth: width, maxWidth: width } : style;
13591
13675
  return /* @__PURE__ */ jsxRuntime.jsx(
13592
13676
  reactNav.Nav,
13593
13677
  {
13594
- selectedValue: selectedValue ?? "",
13678
+ selectedValue: selectedRoute?.to ?? "",
13595
13679
  onNavItemSelect: handleSelect,
13596
- defaultOpenCategories: defaultOpen,
13680
+ openCategories,
13681
+ onNavCategoryItemToggle: handleCategoryToggle,
13597
13682
  className: reactComponents.mergeClasses("ppp-nav-menu", styles.nav, className),
13598
13683
  style: navStyle,
13599
13684
  children
@@ -13623,42 +13708,46 @@ function NavItem({ to, icon, children }) {
13623
13708
  }
13624
13709
  function collectNavTree(children) {
13625
13710
  const allRoutes = [];
13626
- const defaultOpen = [];
13627
- const visit = (nodes) => {
13711
+ const groups = [];
13712
+ const defaultOpenIndexes = [];
13713
+ const visit = (nodes, groupIndex) => {
13628
13714
  react.Children.forEach(nodes, (child) => {
13629
13715
  if (!react.isValidElement(child)) return;
13630
13716
  if (child.type === NavGroup) {
13631
13717
  const props = child.props;
13632
13718
  const value = props.value ?? `group:${props.label}`;
13633
- if (props.defaultExpanded) defaultOpen.push(value);
13634
- visit(props.children);
13719
+ const index = groups.length;
13720
+ groups.push(value);
13721
+ if (props.defaultExpanded) defaultOpenIndexes.push(index);
13722
+ visit(props.children, index);
13635
13723
  return;
13636
13724
  }
13637
13725
  if (child.type === NavItem) {
13638
13726
  const props = child.props;
13639
13727
  allRoutes.push({
13640
13728
  to: props.to,
13641
- match: props.match ?? (props.to === "/" ? "exact" : "prefix")
13729
+ match: props.match ?? (props.to === "/" ? "exact" : "prefix"),
13730
+ groupIndex
13642
13731
  });
13643
13732
  return;
13644
13733
  }
13645
13734
  const wrapperChildren = child.props.children;
13646
- if (wrapperChildren !== void 0) visit(wrapperChildren);
13735
+ if (wrapperChildren !== void 0) visit(wrapperChildren, groupIndex);
13647
13736
  });
13648
13737
  };
13649
- visit(children);
13650
- return { allRoutes, defaultOpen };
13738
+ visit(children, -1);
13739
+ return { allRoutes, groups, defaultOpenIndexes };
13651
13740
  }
13652
13741
  function bestRouteMatch(routes, pathname) {
13653
13742
  const normalize = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
13654
13743
  const target = normalize(pathname);
13655
13744
  let best;
13656
13745
  let bestLen = -1;
13657
- for (const { to, match } of routes) {
13658
- const route = normalize(to);
13659
- const matches = route === target || match === "prefix" && (route === "/" || target.startsWith(route + "/"));
13746
+ for (const candidate of routes) {
13747
+ const route = normalize(candidate.to);
13748
+ const matches = route === target || candidate.match === "prefix" && (route === "/" || target.startsWith(route + "/"));
13660
13749
  if (matches && route.length > bestLen) {
13661
- best = to;
13750
+ best = candidate;
13662
13751
  bestLen = route.length;
13663
13752
  }
13664
13753
  }
@@ -14401,8 +14490,40 @@ var useStyles12 = reactComponents.makeStyles({
14401
14490
  mergedButtons: {
14402
14491
  display: "flex",
14403
14492
  flexWrap: "wrap",
14404
- gap: reactComponents.tokens.spacingHorizontalS
14493
+ gap: reactComponents.tokens.spacingHorizontalL
14494
+ },
14495
+ // One culture group: language label + inspect button + download button, kept together.
14496
+ mergedCulture: {
14497
+ display: "flex",
14498
+ alignItems: "center",
14499
+ gap: reactComponents.tokens.spacingHorizontalXS
14500
+ },
14501
+ mergedCultureLabel: { fontSize: reactComponents.tokens.fontSizeBase200 },
14502
+ dialogSurface: { maxWidth: "1100px", width: "90vw" },
14503
+ dialogToolbar: {
14504
+ display: "flex",
14505
+ alignItems: "center",
14506
+ gap: reactComponents.tokens.spacingHorizontalM,
14507
+ flexWrap: "wrap",
14508
+ marginBottom: reactComponents.tokens.spacingVerticalS
14509
+ },
14510
+ // Scroll viewport for the merged grid so a tall result set scrolls inside the dialog.
14511
+ dialogGrid: { maxHeight: "60vh", overflowY: "auto", overflowX: "auto" },
14512
+ // Merged-snapshot cells. Keys and sources are long single tokens (dotted keys,
14513
+ // backslash-only absolute paths) with no natural break opportunity, so without
14514
+ // this the text paints straight over the neighbouring column. `overflow-wrap:
14515
+ // anywhere` (not just `break-word`) is what makes the flex cell honour its
14516
+ // column width — it counts the soft breaks toward min-content — so the token
14517
+ // wraps inside the column instead of stretching it. Same recipe as
14518
+ // `sourceCell` in the per-source loads table above.
14519
+ mergedCell: {
14520
+ whiteSpace: "normal",
14521
+ overflowWrap: "anywhere",
14522
+ wordBreak: "break-word"
14405
14523
  },
14524
+ filterInput: { minWidth: "260px" },
14525
+ pager: { display: "flex", alignItems: "center", gap: reactComponents.tokens.spacingHorizontalS },
14526
+ grow: { flex: 1 },
14406
14527
  sectionBody: {
14407
14528
  display: "flex",
14408
14529
  flexDirection: "column",
@@ -14421,6 +14542,15 @@ function downloadBytes(fileName, data, contentType = "application/json") {
14421
14542
  anchor.click();
14422
14543
  URL.revokeObjectURL(url);
14423
14544
  }
14545
+ var kindKeyOf = (kind) => KIND_KEY[Number(kind)] ?? "tablemetadata";
14546
+ function cultureLabel(culture) {
14547
+ try {
14548
+ const name = new Intl.DisplayNames(void 0, { type: "language" }).of(culture);
14549
+ return name && name.toLowerCase() !== culture.toLowerCase() ? `${name} (${culture})` : culture;
14550
+ } catch {
14551
+ return culture;
14552
+ }
14553
+ }
14424
14554
  function LocalizationAdmin({ className }) {
14425
14555
  const styles = useStyles12();
14426
14556
  const t = react$1.useT();
@@ -14430,6 +14560,7 @@ function LocalizationAdmin({ className }) {
14430
14560
  const [isReloading, setIsReloading] = react.useState(false);
14431
14561
  const [isDownloading, setIsDownloading] = react.useState(false);
14432
14562
  const [message, setMessage] = react.useState();
14563
+ const [mergedDialogCulture, setMergedDialogCulture] = react.useState(null);
14433
14564
  const loadOverview = react.useCallback(async () => {
14434
14565
  try {
14435
14566
  const value = await ppp.getLocalizationOverviewAsync();
@@ -14587,17 +14718,44 @@ function LocalizationAdmin({ className }) {
14587
14718
  /* @__PURE__ */ jsxRuntime.jsx(LocalizationTranslator, {}),
14588
14719
  availableCultures.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(Section, { headerText: t("app.localization-admin.download-merged-title"), borderVisible: true, children: /* @__PURE__ */ jsxRuntime.jsx(SectionColumn, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.sectionBody, children: [
14589
14720
  /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Text, { children: t("app.localization-admin.download-merged-description") }),
14590
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.mergedButtons, children: availableCultures.map((culture) => /* @__PURE__ */ jsxRuntime.jsx(
14591
- reactComponents.Button,
14592
- {
14593
- appearance: "outline",
14594
- disabled: isDownloading,
14595
- icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.ArrowDownloadRegular, {}),
14596
- onClick: () => onDownloadMerged(culture),
14597
- children: culture
14598
- },
14599
- culture
14600
- )) })
14721
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.mergedButtons, children: availableCultures.map((culture) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.mergedCulture, children: [
14722
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.mergedCultureLabel, children: cultureLabel(culture) }),
14723
+ /* @__PURE__ */ jsxRuntime.jsx(
14724
+ reactComponents.Tooltip,
14725
+ {
14726
+ content: t("app.localization-admin.view-merged-tooltip", [
14727
+ cultureLabel(culture)
14728
+ ]),
14729
+ relationship: "label",
14730
+ children: /* @__PURE__ */ jsxRuntime.jsx(
14731
+ reactComponents.Button,
14732
+ {
14733
+ appearance: "outline",
14734
+ icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.EyeRegular, {}),
14735
+ onClick: () => setMergedDialogCulture(culture)
14736
+ }
14737
+ )
14738
+ }
14739
+ ),
14740
+ /* @__PURE__ */ jsxRuntime.jsx(
14741
+ reactComponents.Tooltip,
14742
+ {
14743
+ content: t("app.localization-admin.download-merged-tooltip", [
14744
+ cultureLabel(culture)
14745
+ ]),
14746
+ relationship: "label",
14747
+ children: /* @__PURE__ */ jsxRuntime.jsx(
14748
+ reactComponents.Button,
14749
+ {
14750
+ appearance: "subtle",
14751
+ disabled: isDownloading,
14752
+ icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.ArrowDownloadRegular, {}),
14753
+ onClick: () => onDownloadMerged(culture)
14754
+ }
14755
+ )
14756
+ }
14757
+ )
14758
+ ] }, culture)) })
14601
14759
  ] }) }) }),
14602
14760
  /* @__PURE__ */ jsxRuntime.jsx(Section, { headerText: t("app.localization-admin.sources-title"), borderVisible: true, children: /* @__PURE__ */ jsxRuntime.jsx(SectionColumn, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.sectionBody, children: [
14603
14761
  /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Text, { children: t("app.localization-admin.sources-description") }),
@@ -14628,7 +14786,22 @@ function LocalizationAdmin({ className }) {
14628
14786
  ] }) }) })
14629
14787
  ] });
14630
14788
  }
14631
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: reactComponents.mergeClasses("ppp-localization-admin", styles.root, className), children: content });
14789
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: reactComponents.mergeClasses("ppp-localization-admin", styles.root, className), children: [
14790
+ content,
14791
+ mergedDialogCulture !== null && /* @__PURE__ */ jsxRuntime.jsx(
14792
+ MergedSourcesDialog,
14793
+ {
14794
+ culture: mergedDialogCulture,
14795
+ label: cultureLabel(mergedDialogCulture),
14796
+ onClose: () => setMergedDialogCulture(null),
14797
+ onDownload: () => onDownloadMerged(mergedDialogCulture),
14798
+ isDownloading,
14799
+ ppp,
14800
+ t,
14801
+ styles
14802
+ }
14803
+ )
14804
+ ] });
14632
14805
  }
14633
14806
  function ConfigRow({ styles, label, value }) {
14634
14807
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -14678,6 +14851,177 @@ function SourceRow({
14678
14851
  )) }) })
14679
14852
  ] });
14680
14853
  }
14854
+ var MERGED_PAGE_SIZE = 100;
14855
+ function MergedSourcesDialog({
14856
+ culture,
14857
+ label,
14858
+ onClose,
14859
+ onDownload,
14860
+ isDownloading,
14861
+ ppp,
14862
+ t,
14863
+ styles
14864
+ }) {
14865
+ const [entries, setEntries] = react.useState(null);
14866
+ const [loadError, setLoadError] = react.useState();
14867
+ const [filter, setFilter] = react.useState("");
14868
+ const [page, setPage] = react.useState(0);
14869
+ const [sortState, setSortState] = react.useState({
14870
+ sortColumn: "key",
14871
+ sortDirection: "ascending"
14872
+ });
14873
+ react.useEffect(() => {
14874
+ let cancelled = false;
14875
+ void (async () => {
14876
+ try {
14877
+ const value = await ppp.getMergedLocalizationEntriesAsync(culture);
14878
+ if (!cancelled) {
14879
+ setEntries(value);
14880
+ setLoadError(void 0);
14881
+ }
14882
+ } catch (reason) {
14883
+ if (!cancelled) {
14884
+ setEntries(null);
14885
+ setLoadError(reason instanceof Error ? reason.message : String(reason));
14886
+ }
14887
+ }
14888
+ })();
14889
+ return () => {
14890
+ cancelled = true;
14891
+ };
14892
+ }, [ppp, culture]);
14893
+ const columns = react.useMemo(
14894
+ () => [
14895
+ reactComponents.createTableColumn({
14896
+ columnId: "key",
14897
+ compare: (a, b) => a.key.localeCompare(b.key),
14898
+ renderHeaderCell: () => t("app.localization-admin.merged-column-key"),
14899
+ renderCell: (item) => item.key
14900
+ }),
14901
+ reactComponents.createTableColumn({
14902
+ columnId: "kind",
14903
+ compare: (a, b) => Number(a.sourceKind) - Number(b.sourceKind),
14904
+ renderHeaderCell: () => t("app.localization-admin.merged-column-kind"),
14905
+ renderCell: (item) => t(`app.localization-admin.kind-${kindKeyOf(item.sourceKind)}`)
14906
+ }),
14907
+ reactComponents.createTableColumn({
14908
+ columnId: "source",
14909
+ compare: (a, b) => a.sourceDisplayName.localeCompare(b.sourceDisplayName),
14910
+ renderHeaderCell: () => t("app.localization-admin.merged-column-source"),
14911
+ renderCell: (item) => item.sourceDisplayName
14912
+ }),
14913
+ reactComponents.createTableColumn({
14914
+ columnId: "value",
14915
+ compare: (a, b) => a.value.localeCompare(b.value),
14916
+ renderHeaderCell: () => t("app.localization-admin.merged-column-value"),
14917
+ renderCell: (item) => item.value
14918
+ })
14919
+ ],
14920
+ [t]
14921
+ );
14922
+ const filtered = react.useMemo(() => {
14923
+ if (!entries) return [];
14924
+ const q = filter.trim().toLowerCase();
14925
+ const base = q ? entries.filter(
14926
+ (e) => e.key.toLowerCase().includes(q) || e.value.toLowerCase().includes(q) || e.sourceDisplayName.toLowerCase().includes(q)
14927
+ ) : entries;
14928
+ return [...base];
14929
+ }, [entries, filter]);
14930
+ const sorted = react.useMemo(() => {
14931
+ const column = columns.find((c) => c.columnId === sortState.sortColumn);
14932
+ if (!column?.compare) return filtered;
14933
+ const sign = sortState.sortDirection === "ascending" ? 1 : -1;
14934
+ return [...filtered].sort((a, b) => sign * column.compare(a, b));
14935
+ }, [filtered, columns, sortState]);
14936
+ react.useEffect(() => {
14937
+ setPage(0);
14938
+ }, [filter, sortState]);
14939
+ const pageCount = Math.max(1, Math.ceil(sorted.length / MERGED_PAGE_SIZE));
14940
+ const currentPage = Math.min(page, pageCount - 1);
14941
+ const paged = sorted.slice(currentPage * MERGED_PAGE_SIZE, currentPage * MERGED_PAGE_SIZE + MERGED_PAGE_SIZE);
14942
+ const columnSizingOptions = react.useMemo(
14943
+ () => ({
14944
+ key: { minWidth: 200, defaultWidth: 320 },
14945
+ kind: { minWidth: 110, defaultWidth: 140 },
14946
+ source: { minWidth: 200, defaultWidth: 300 },
14947
+ value: { minWidth: 200, defaultWidth: 360 }
14948
+ }),
14949
+ []
14950
+ );
14951
+ return /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Dialog, { open: true, onOpenChange: (_event, data) => !data.open && onClose(), children: /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DialogSurface, { className: styles.dialogSurface, children: /* @__PURE__ */ jsxRuntime.jsxs(reactComponents.DialogBody, { children: [
14952
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DialogTitle, { children: t("app.localization-admin.merged-dialog-title", [label]) }),
14953
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DialogContent, { children: loadError !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactComponents.MessageBar, { intent: "error", children: /* @__PURE__ */ jsxRuntime.jsx(reactComponents.MessageBarBody, { children: t("app.localization-admin.merged-load-failed", [loadError]) }) }) : entries === null ? /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Spinner, {}) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
14954
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.dialogToolbar, children: [
14955
+ /* @__PURE__ */ jsxRuntime.jsx(
14956
+ reactComponents.Input,
14957
+ {
14958
+ className: styles.filterInput,
14959
+ value: filter,
14960
+ onChange: (_event, data) => setFilter(data.value),
14961
+ contentBefore: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.SearchRegular, {}),
14962
+ placeholder: t("app.localization-admin.merged-filter-placeholder")
14963
+ }
14964
+ ),
14965
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.lastReloaded, children: t("app.localization-admin.merged-count", [sorted.length, entries.length]) }),
14966
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.grow }),
14967
+ pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles.pager, children: [
14968
+ /* @__PURE__ */ jsxRuntime.jsx(
14969
+ reactComponents.Button,
14970
+ {
14971
+ size: "small",
14972
+ appearance: "subtle",
14973
+ disabled: currentPage === 0,
14974
+ onClick: () => setPage(currentPage - 1),
14975
+ children: t("app.localization-admin.merged-prev")
14976
+ }
14977
+ ),
14978
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.lastReloaded, children: t("app.localization-admin.merged-page", [currentPage + 1, pageCount]) }),
14979
+ /* @__PURE__ */ jsxRuntime.jsx(
14980
+ reactComponents.Button,
14981
+ {
14982
+ size: "small",
14983
+ appearance: "subtle",
14984
+ disabled: currentPage >= pageCount - 1,
14985
+ onClick: () => setPage(currentPage + 1),
14986
+ children: t("app.localization-admin.merged-next")
14987
+ }
14988
+ )
14989
+ ] })
14990
+ ] }),
14991
+ sorted.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactComponents.MessageBar, { intent: "info", children: /* @__PURE__ */ jsxRuntime.jsx(reactComponents.MessageBarBody, { children: t("app.localization-admin.merged-empty") }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.dialogGrid, children: /* @__PURE__ */ jsxRuntime.jsxs(
14992
+ reactComponents.DataGrid,
14993
+ {
14994
+ items: paged,
14995
+ columns,
14996
+ sortable: true,
14997
+ resizableColumns: true,
14998
+ sortState,
14999
+ onSortChange: (_event, next) => setSortState(next),
15000
+ columnSizingOptions,
15001
+ getRowId: (item) => item.key,
15002
+ size: "small",
15003
+ children: [
15004
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DataGridHeader, { children: /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DataGridRow, { children: ({ renderHeaderCell }) => /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DataGridHeaderCell, { children: renderHeaderCell() }) }) }),
15005
+ /* @__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) })
15006
+ ]
15007
+ }
15008
+ ) })
15009
+ ] }) }),
15010
+ /* @__PURE__ */ jsxRuntime.jsxs(reactComponents.DialogActions, { children: [
15011
+ /* @__PURE__ */ jsxRuntime.jsx(
15012
+ reactComponents.Button,
15013
+ {
15014
+ appearance: "primary",
15015
+ icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.ArrowDownloadRegular, {}),
15016
+ disabled: isDownloading,
15017
+ onClick: onDownload,
15018
+ children: t("app.localization-admin.merged-download-button")
15019
+ }
15020
+ ),
15021
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Button, { appearance: "secondary", icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.DismissRegular, {}), onClick: onClose, children: t("app.buttons.close.label") })
15022
+ ] })
15023
+ ] }) }) });
15024
+ }
14681
15025
  var useStyles13 = reactComponents.makeStyles({
14682
15026
  root: {
14683
15027
  display: "flex",