@powerportalspro/react-fluent 5.0.0 → 6.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
@@ -2483,6 +2483,7 @@ var ImageViewer = react.forwardRef(function ImageViewer2({
2483
2483
  const selectionRef = react.useRef(selection);
2484
2484
  selectionRef.current = selection;
2485
2485
  const prevTransformRef = react.useRef({ angle, zoom, pan, flipH, flipV });
2486
+ const resizingRef = react.useRef(false);
2486
2487
  const dragRef = react.useRef(null);
2487
2488
  const [isDragging, setIsDragging] = react.useState(false);
2488
2489
  const zoomRef = react.useRef(zoom);
@@ -2512,6 +2513,7 @@ var ImageViewer = react.forwardRef(function ImageViewer2({
2512
2513
  const cur = { angle, zoom, pan, flipH, flipV };
2513
2514
  const transformChanged = prev.angle !== cur.angle || prev.zoom !== cur.zoom || prev.pan.x !== cur.pan.x || prev.pan.y !== cur.pan.y || prev.flipH !== cur.flipH || prev.flipV !== cur.flipV;
2514
2515
  if (transformChanged) {
2516
+ resizingRef.current = false;
2515
2517
  const sel = selectionRef.current;
2516
2518
  const imgEl = imgRef.current;
2517
2519
  if (sel && imgEl) {
@@ -2542,6 +2544,51 @@ var ImageViewer = react.forwardRef(function ImageViewer2({
2542
2544
  }
2543
2545
  prevTransformRef.current = cur;
2544
2546
  }, [angle, zoom, pan, flipH, flipV]);
2547
+ react.useEffect(() => {
2548
+ if (!toolbar) return;
2549
+ const container = imageContainerRef.current;
2550
+ if (!container) return;
2551
+ let prevMetrics = imgRef.current ? readImageLayoutMetrics(imgRef.current) : null;
2552
+ const observer = new ResizeObserver(() => {
2553
+ const imgEl = imgRef.current;
2554
+ if (!imgEl) return;
2555
+ const newMetrics = readImageLayoutMetrics(imgEl);
2556
+ if (!newMetrics) return;
2557
+ const sel = selectionRef.current;
2558
+ const layoutChanged = !prevMetrics || prevMetrics.layoutW !== newMetrics.layoutW || prevMetrics.layoutH !== newMetrics.layoutH || prevMetrics.offsetLeft !== newMetrics.offsetLeft || prevMetrics.offsetTop !== newMetrics.offsetTop;
2559
+ if (sel && prevMetrics && layoutChanged) {
2560
+ const natural = containerRectToImageNatural(
2561
+ sel,
2562
+ imgEl,
2563
+ angleRef.current,
2564
+ zoomRef.current,
2565
+ panRef.current,
2566
+ flipHRef.current,
2567
+ flipVRef.current,
2568
+ prevMetrics
2569
+ );
2570
+ if (natural) {
2571
+ const reprojected = imageNaturalRectToContainer(
2572
+ natural,
2573
+ imgEl,
2574
+ angleRef.current,
2575
+ zoomRef.current,
2576
+ panRef.current,
2577
+ flipHRef.current,
2578
+ flipVRef.current,
2579
+ newMetrics
2580
+ );
2581
+ if (reprojected) {
2582
+ resizingRef.current = true;
2583
+ setSelection(reprojected);
2584
+ }
2585
+ }
2586
+ }
2587
+ prevMetrics = newMetrics;
2588
+ });
2589
+ observer.observe(container);
2590
+ return () => observer.disconnect();
2591
+ }, [toolbar]);
2545
2592
  const onDirtyChangedRef = react.useRef(onDirtyChanged);
2546
2593
  onDirtyChangedRef.current = onDirtyChanged;
2547
2594
  react.useEffect(() => {
@@ -3295,7 +3342,7 @@ var ImageViewer = react.forwardRef(function ImageViewer2({
3295
3342
  // pointermove (which is what changes the selection during
3296
3343
  // a drag) calls setSelection — that triggers a re-render
3297
3344
  // which reads the current dragRef value.
3298
- transition: dragRef.current ? "none" : "left 120ms ease-out, top 120ms ease-out, width 120ms ease-out, height 120ms ease-out"
3345
+ transition: dragRef.current || resizingRef.current ? "none" : "left 120ms ease-out, top 120ms ease-out, width 120ms ease-out, height 120ms ease-out"
3299
3346
  },
3300
3347
  children: [
3301
3348
  showThirds && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.thirdsGuide, "aria-hidden": "true", children: [
@@ -3456,6 +3503,21 @@ function useImageSrc(source) {
3456
3503
  return source.value ? `data:${resolveMimeType(source)};base64,${source.value}` : null;
3457
3504
  return bytesUrlRef.current;
3458
3505
  }
3506
+ function readImageLayoutMetrics(imageEl) {
3507
+ const layoutW = imageEl.offsetWidth;
3508
+ const layoutH = imageEl.offsetHeight;
3509
+ const naturalW = imageEl.naturalWidth;
3510
+ const naturalH = imageEl.naturalHeight;
3511
+ if (layoutW === 0 || layoutH === 0 || naturalW === 0 || naturalH === 0) return null;
3512
+ return {
3513
+ layoutW,
3514
+ layoutH,
3515
+ offsetLeft: imageEl.offsetLeft,
3516
+ offsetTop: imageEl.offsetTop,
3517
+ naturalW,
3518
+ naturalH
3519
+ };
3520
+ }
3459
3521
  function getDisplayedImageBounds(imageEl, angle, zoom, pan, flipH, flipV) {
3460
3522
  const layoutW = imageEl.offsetWidth;
3461
3523
  const layoutH = imageEl.offsetHeight;
@@ -3498,15 +3560,13 @@ function getDisplayedImageBounds(imageEl, angle, zoom, pan, flipH, flipV) {
3498
3560
  }
3499
3561
  return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3500
3562
  }
3501
- function containerRectToImageNatural(rect, imageEl, angle, zoom, pan, flipH, flipV) {
3502
- const layoutW = imageEl.offsetWidth;
3503
- const layoutH = imageEl.offsetHeight;
3504
- const naturalW = imageEl.naturalWidth;
3505
- const naturalH = imageEl.naturalHeight;
3506
- if (layoutW === 0 || layoutH === 0 || naturalW === 0 || naturalH === 0) return null;
3563
+ function containerRectToImageNatural(rect, imageEl, angle, zoom, pan, flipH, flipV, metricsOverride) {
3564
+ const m = metricsOverride ?? readImageLayoutMetrics(imageEl);
3565
+ if (!m) return null;
3566
+ const { layoutW, layoutH, naturalW, naturalH } = m;
3507
3567
  const containScale = Math.min(layoutW / naturalW, layoutH / naturalH);
3508
- const imgCenterX = imageEl.offsetLeft + layoutW / 2;
3509
- const imgCenterY = imageEl.offsetTop + layoutH / 2;
3568
+ const imgCenterX = m.offsetLeft + layoutW / 2;
3569
+ const imgCenterY = m.offsetTop + layoutH / 2;
3510
3570
  const sx = zoom * (flipH ? -1 : 1);
3511
3571
  const sy = zoom * (flipV ? -1 : 1);
3512
3572
  if (sx === 0 || sy === 0) return null;
@@ -3539,15 +3599,13 @@ function containerRectToImageNatural(rect, imageEl, angle, zoom, pan, flipH, fli
3539
3599
  }
3540
3600
  return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3541
3601
  }
3542
- function imageNaturalRectToContainer(rect, imageEl, angle, zoom, pan, flipH, flipV) {
3543
- const layoutW = imageEl.offsetWidth;
3544
- const layoutH = imageEl.offsetHeight;
3545
- const naturalW = imageEl.naturalWidth;
3546
- const naturalH = imageEl.naturalHeight;
3547
- if (layoutW === 0 || layoutH === 0 || naturalW === 0 || naturalH === 0) return null;
3602
+ function imageNaturalRectToContainer(rect, imageEl, angle, zoom, pan, flipH, flipV, metricsOverride) {
3603
+ const m = metricsOverride ?? readImageLayoutMetrics(imageEl);
3604
+ if (!m) return null;
3605
+ const { layoutW, layoutH, naturalW, naturalH } = m;
3548
3606
  const containScale = Math.min(layoutW / naturalW, layoutH / naturalH);
3549
- const imgCenterX = imageEl.offsetLeft + layoutW / 2;
3550
- const imgCenterY = imageEl.offsetTop + layoutH / 2;
3607
+ const imgCenterX = m.offsetLeft + layoutW / 2;
3608
+ const imgCenterY = m.offsetTop + layoutH / 2;
3551
3609
  const sx = zoom * (flipH ? -1 : 1);
3552
3610
  const sy = zoom * (flipV ? -1 : 1);
3553
3611
  const rad = angle * Math.PI / 180;
@@ -4425,6 +4483,69 @@ function ZipViewer({
4425
4483
  }
4426
4484
  );
4427
4485
  }
4486
+ function ZipEntryIcon({ fileName, className }) {
4487
+ switch (getFileExtension(fileName)) {
4488
+ case "pdf":
4489
+ return /* @__PURE__ */ jsxRuntime.jsx(reactIcons.DocumentPdf20Regular, { className });
4490
+ case "xls":
4491
+ case "xlsx":
4492
+ case "csv":
4493
+ case "ods":
4494
+ return /* @__PURE__ */ jsxRuntime.jsx(reactIcons.DocumentTable20Regular, { className });
4495
+ case "ppt":
4496
+ case "pptx":
4497
+ case "odp":
4498
+ return /* @__PURE__ */ jsxRuntime.jsx(reactIcons.SlideText20Regular, { className });
4499
+ case "png":
4500
+ case "jpg":
4501
+ case "jpeg":
4502
+ case "gif":
4503
+ case "bmp":
4504
+ case "webp":
4505
+ case "svg":
4506
+ case "ico":
4507
+ case "tif":
4508
+ case "tiff":
4509
+ return /* @__PURE__ */ jsxRuntime.jsx(reactIcons.Image20Regular, { className });
4510
+ case "mp4":
4511
+ case "mov":
4512
+ case "avi":
4513
+ case "mkv":
4514
+ case "webm":
4515
+ case "m4v":
4516
+ return /* @__PURE__ */ jsxRuntime.jsx(reactIcons.Video20Regular, { className });
4517
+ case "mp3":
4518
+ case "wav":
4519
+ case "ogg":
4520
+ case "flac":
4521
+ case "m4a":
4522
+ case "aac":
4523
+ return /* @__PURE__ */ jsxRuntime.jsx(reactIcons.MusicNote220Regular, { className });
4524
+ case "zip":
4525
+ case "7z":
4526
+ case "rar":
4527
+ case "tar":
4528
+ case "gz":
4529
+ return /* @__PURE__ */ jsxRuntime.jsx(reactIcons.FolderZip20Regular, { className });
4530
+ case "txt":
4531
+ case "md":
4532
+ case "markdown":
4533
+ case "log":
4534
+ case "json":
4535
+ case "xml":
4536
+ case "yaml":
4537
+ case "yml":
4538
+ case "html":
4539
+ case "htm":
4540
+ case "js":
4541
+ case "ts":
4542
+ case "cs":
4543
+ case "css":
4544
+ return /* @__PURE__ */ jsxRuntime.jsx(reactIcons.DocumentText20Regular, { className });
4545
+ default:
4546
+ return /* @__PURE__ */ jsxRuntime.jsx(reactIcons.Document20Regular, { className });
4547
+ }
4548
+ }
4428
4549
  function ZipTreeNode({ node, depth, expanded, selected, onToggle, onSelect }) {
4429
4550
  const styles = useFileViewerStyles();
4430
4551
  const isExpanded = expanded.has(node.fullPath);
@@ -4483,7 +4604,7 @@ function ZipTreeNode({ node, depth, expanded, selected, onToggle, onSelect }) {
4483
4604
  style: indent,
4484
4605
  children: [
4485
4606
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.zipChevronSpacer }),
4486
- /* @__PURE__ */ jsxRuntime.jsx(reactIcons.Document20Regular, { className: styles.zipIcon }),
4607
+ /* @__PURE__ */ jsxRuntime.jsx(ZipEntryIcon, { fileName: node.name, className: styles.zipIcon }),
4487
4608
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.zipName, children: node.name })
4488
4609
  ]
4489
4610
  }
@@ -5128,11 +5249,13 @@ var useFileViewerStyles = reactComponents.makeStyles({
5128
5249
  outlineOffset: "-1px"
5129
5250
  }
5130
5251
  },
5252
+ // Selection style ported from the Blazor zip viewer: a subtle accent tint over
5253
+ // the row (text stays its normal color), intensifying slightly on hover so the
5254
+ // neutral row-hover doesn't wash out the selection.
5131
5255
  zipRowSelected: {
5132
- backgroundColor: reactComponents.tokens.colorBrandBackground2,
5133
- color: reactComponents.tokens.colorBrandForeground2,
5256
+ backgroundColor: `color-mix(in srgb, ${reactComponents.tokens.colorBrandBackground} 12%, transparent)`,
5134
5257
  ":hover": {
5135
- backgroundColor: reactComponents.tokens.colorBrandBackground2Hover
5258
+ backgroundColor: `color-mix(in srgb, ${reactComponents.tokens.colorBrandBackground} 18%, transparent)`
5136
5259
  }
5137
5260
  },
5138
5261
  zipChevron: {
@@ -8907,7 +9030,13 @@ function MainGridImpl({
8907
9030
  (row) => row.properties?.[columnName],
8908
9031
  {
8909
9032
  id: columnName,
8910
- header: () => resolvedColumn.displayName,
9033
+ // Headers normally show the server-resolved Dataverse display name, which bypasses the
9034
+ // client `t()` — so the ?ppp-show-keys aid couldn't reach them. Consult key-mode here too:
9035
+ // when it's on, show the column's localization key (the one a consumer would override),
9036
+ // keyed by the column's OWN owning table (`resolvedColumn.tableName` — the related table
9037
+ // for a linked-entity column, not the grid's primary table). For linked columns the name
9038
+ // is a dotted alias path, so take the segment after the last dot as the real column name.
9039
+ header: () => react$1.isKeyModeEnabled() ? `tables.${resolvedColumn.tableName}.columns.${columnName.split(".").pop() ?? columnName}.label` : resolvedColumn.displayName,
8911
9040
  cell: (cellCtx) => {
8912
9041
  const rowRecord = cellCtx.row.original;
8913
9042
  const rowId = rowRecord.id;
@@ -11671,15 +11800,11 @@ function ManyToManyLookupEdit({
11671
11800
  return relatedPrimaryName;
11672
11801
  }, [resolvedColumns, relatedPrimaryName]);
11673
11802
  const lastSeededRecordIdRef = react.useRef(void 0);
11674
- const lastSeededViewIdRef = react.useRef(void 0);
11675
11803
  react.useEffect(() => {
11676
11804
  if (initialLoad.status !== react$1.QueryStatus.Success || !initialLoad.data) return;
11677
11805
  if (!primaryNameColumn || !parentRecord) return;
11678
- if (lastSeededRecordIdRef.current === parentRefId && lastSeededViewIdRef.current === selectedViewId) {
11679
- return;
11680
- }
11806
+ if (lastSeededRecordIdRef.current === parentRefId) return;
11681
11807
  lastSeededRecordIdRef.current = parentRefId;
11682
- lastSeededViewIdRef.current = selectedViewId;
11683
11808
  const seeded = initialLoad.data.tableRecords.map((r) => ({
11684
11809
  id: r.id,
11685
11810
  displayName: resolveDisplayName(r, primaryNameColumn) ?? "",
@@ -11688,7 +11813,7 @@ function ManyToManyLookupEdit({
11688
11813
  setInitialSelection(seeded);
11689
11814
  setPendingAdds([]);
11690
11815
  setPendingRemoveIds([]);
11691
- }, [initialLoad.status, initialLoad.data, parentRecord, primaryNameColumn, selectedViewId]);
11816
+ }, [initialLoad.status, initialLoad.data, parentRecord, primaryNameColumn, parentRefId]);
11692
11817
  const [query, setQuery] = react.useState("");
11693
11818
  const [debouncedQuery, setDebouncedQuery] = react.useState("");
11694
11819
  const [searchResults, setSearchResults] = react.useState([]);
@@ -11800,7 +11925,6 @@ function ManyToManyLookupEdit({
11800
11925
  setPendingAdds([]);
11801
11926
  setPendingRemoveIds([]);
11802
11927
  lastSeededRecordIdRef.current = void 0;
11803
- lastSeededViewIdRef.current = void 0;
11804
11928
  },
11805
11929
  // Cancel/Discard: just drop the pending adds/removes; the
11806
11930
  // seed-guard refs stay so the initial selection (already loaded
@@ -13455,27 +13579,57 @@ function NavMenu({ children, width, className, style }) {
13455
13579
  useNavSelectedStyles();
13456
13580
  const location = reactRouterDom.useLocation();
13457
13581
  const navigate = reactRouterDom.useNavigate();
13458
- const { allRoutes, defaultOpen } = react.useMemo(
13582
+ const { allRoutes, groups, defaultOpenIndexes } = react.useMemo(
13459
13583
  () => collectNavTree(children),
13460
13584
  [children]
13461
13585
  );
13462
- const selectedValue = react.useMemo(
13586
+ const selectedRoute = react.useMemo(
13463
13587
  () => bestRouteMatch(allRoutes, location.pathname),
13464
13588
  [allRoutes, location.pathname]
13465
13589
  );
13590
+ const [openIndexes, setOpenIndexes] = react.useState(
13591
+ () => /* @__PURE__ */ new Set([
13592
+ ...defaultOpenIndexes,
13593
+ // Seed the active route's group too, so it's open on first paint
13594
+ // (the effect below only fires after mount).
13595
+ ...selectedRoute && selectedRoute.groupIndex >= 0 ? [selectedRoute.groupIndex] : []
13596
+ ])
13597
+ );
13598
+ const selectedGroupIndex = selectedRoute?.groupIndex ?? -1;
13599
+ react.useEffect(() => {
13600
+ if (selectedGroupIndex < 0) return;
13601
+ setOpenIndexes(
13602
+ (prev) => prev.has(selectedGroupIndex) ? prev : new Set(prev).add(selectedGroupIndex)
13603
+ );
13604
+ }, [selectedGroupIndex]);
13605
+ const openCategories = react.useMemo(
13606
+ () => groups.filter((_, index) => openIndexes.has(index)),
13607
+ [groups, openIndexes]
13608
+ );
13466
13609
  const handleSelect = (_, data) => {
13467
13610
  const value = data.value;
13468
13611
  if (typeof value === "string" && value.startsWith("/")) {
13469
13612
  navigate(value);
13470
13613
  }
13471
13614
  };
13615
+ const handleCategoryToggle = (_, data) => {
13616
+ const index = groups.indexOf(data.value);
13617
+ if (index < 0) return;
13618
+ setOpenIndexes((prev) => {
13619
+ const next = new Set(prev);
13620
+ if (next.has(index)) next.delete(index);
13621
+ else next.add(index);
13622
+ return next;
13623
+ });
13624
+ };
13472
13625
  const navStyle = width ? { ...style, minWidth: width, maxWidth: width } : style;
13473
13626
  return /* @__PURE__ */ jsxRuntime.jsx(
13474
13627
  reactNav.Nav,
13475
13628
  {
13476
- selectedValue: selectedValue ?? "",
13629
+ selectedValue: selectedRoute?.to ?? "",
13477
13630
  onNavItemSelect: handleSelect,
13478
- defaultOpenCategories: defaultOpen,
13631
+ openCategories,
13632
+ onNavCategoryItemToggle: handleCategoryToggle,
13479
13633
  className: reactComponents.mergeClasses("ppp-nav-menu", styles.nav, className),
13480
13634
  style: navStyle,
13481
13635
  children
@@ -13505,42 +13659,46 @@ function NavItem({ to, icon, children }) {
13505
13659
  }
13506
13660
  function collectNavTree(children) {
13507
13661
  const allRoutes = [];
13508
- const defaultOpen = [];
13509
- const visit = (nodes) => {
13662
+ const groups = [];
13663
+ const defaultOpenIndexes = [];
13664
+ const visit = (nodes, groupIndex) => {
13510
13665
  react.Children.forEach(nodes, (child) => {
13511
13666
  if (!react.isValidElement(child)) return;
13512
13667
  if (child.type === NavGroup) {
13513
13668
  const props = child.props;
13514
13669
  const value = props.value ?? `group:${props.label}`;
13515
- if (props.defaultExpanded) defaultOpen.push(value);
13516
- visit(props.children);
13670
+ const index = groups.length;
13671
+ groups.push(value);
13672
+ if (props.defaultExpanded) defaultOpenIndexes.push(index);
13673
+ visit(props.children, index);
13517
13674
  return;
13518
13675
  }
13519
13676
  if (child.type === NavItem) {
13520
13677
  const props = child.props;
13521
13678
  allRoutes.push({
13522
13679
  to: props.to,
13523
- match: props.match ?? (props.to === "/" ? "exact" : "prefix")
13680
+ match: props.match ?? (props.to === "/" ? "exact" : "prefix"),
13681
+ groupIndex
13524
13682
  });
13525
13683
  return;
13526
13684
  }
13527
13685
  const wrapperChildren = child.props.children;
13528
- if (wrapperChildren !== void 0) visit(wrapperChildren);
13686
+ if (wrapperChildren !== void 0) visit(wrapperChildren, groupIndex);
13529
13687
  });
13530
13688
  };
13531
- visit(children);
13532
- return { allRoutes, defaultOpen };
13689
+ visit(children, -1);
13690
+ return { allRoutes, groups, defaultOpenIndexes };
13533
13691
  }
13534
13692
  function bestRouteMatch(routes, pathname) {
13535
13693
  const normalize = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
13536
13694
  const target = normalize(pathname);
13537
13695
  let best;
13538
13696
  let bestLen = -1;
13539
- for (const { to, match } of routes) {
13540
- const route = normalize(to);
13541
- const matches = route === target || match === "prefix" && (route === "/" || target.startsWith(route + "/"));
13697
+ for (const candidate of routes) {
13698
+ const route = normalize(candidate.to);
13699
+ const matches = route === target || candidate.match === "prefix" && (route === "/" || target.startsWith(route + "/"));
13542
13700
  if (matches && route.length > bestLen) {
13543
- best = to;
13701
+ best = candidate;
13544
13702
  bestLen = route.length;
13545
13703
  }
13546
13704
  }
@@ -14283,8 +14441,28 @@ var useStyles12 = reactComponents.makeStyles({
14283
14441
  mergedButtons: {
14284
14442
  display: "flex",
14285
14443
  flexWrap: "wrap",
14286
- gap: reactComponents.tokens.spacingHorizontalS
14444
+ gap: reactComponents.tokens.spacingHorizontalL
14445
+ },
14446
+ // One culture group: language label + inspect button + download button, kept together.
14447
+ mergedCulture: {
14448
+ display: "flex",
14449
+ alignItems: "center",
14450
+ gap: reactComponents.tokens.spacingHorizontalXS
14451
+ },
14452
+ mergedCultureLabel: { fontSize: reactComponents.tokens.fontSizeBase200 },
14453
+ dialogSurface: { maxWidth: "1100px", width: "90vw" },
14454
+ dialogToolbar: {
14455
+ display: "flex",
14456
+ alignItems: "center",
14457
+ gap: reactComponents.tokens.spacingHorizontalM,
14458
+ flexWrap: "wrap",
14459
+ marginBottom: reactComponents.tokens.spacingVerticalS
14287
14460
  },
14461
+ // Scroll viewport for the merged grid so a tall result set scrolls inside the dialog.
14462
+ dialogGrid: { maxHeight: "60vh", overflowY: "auto", overflowX: "auto" },
14463
+ filterInput: { minWidth: "260px" },
14464
+ pager: { display: "flex", alignItems: "center", gap: reactComponents.tokens.spacingHorizontalS },
14465
+ grow: { flex: 1 },
14288
14466
  sectionBody: {
14289
14467
  display: "flex",
14290
14468
  flexDirection: "column",
@@ -14303,6 +14481,15 @@ function downloadBytes(fileName, data, contentType = "application/json") {
14303
14481
  anchor.click();
14304
14482
  URL.revokeObjectURL(url);
14305
14483
  }
14484
+ var kindKeyOf = (kind) => KIND_KEY[Number(kind)] ?? "tablemetadata";
14485
+ function cultureLabel(culture) {
14486
+ try {
14487
+ const name = new Intl.DisplayNames(void 0, { type: "language" }).of(culture);
14488
+ return name && name.toLowerCase() !== culture.toLowerCase() ? `${name} (${culture})` : culture;
14489
+ } catch {
14490
+ return culture;
14491
+ }
14492
+ }
14306
14493
  function LocalizationAdmin({ className }) {
14307
14494
  const styles = useStyles12();
14308
14495
  const t = react$1.useT();
@@ -14312,6 +14499,7 @@ function LocalizationAdmin({ className }) {
14312
14499
  const [isReloading, setIsReloading] = react.useState(false);
14313
14500
  const [isDownloading, setIsDownloading] = react.useState(false);
14314
14501
  const [message, setMessage] = react.useState();
14502
+ const [mergedDialogCulture, setMergedDialogCulture] = react.useState(null);
14315
14503
  const loadOverview = react.useCallback(async () => {
14316
14504
  try {
14317
14505
  const value = await ppp.getLocalizationOverviewAsync();
@@ -14469,17 +14657,44 @@ function LocalizationAdmin({ className }) {
14469
14657
  /* @__PURE__ */ jsxRuntime.jsx(LocalizationTranslator, {}),
14470
14658
  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: [
14471
14659
  /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Text, { children: t("app.localization-admin.download-merged-description") }),
14472
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.mergedButtons, children: availableCultures.map((culture) => /* @__PURE__ */ jsxRuntime.jsx(
14473
- reactComponents.Button,
14474
- {
14475
- appearance: "outline",
14476
- disabled: isDownloading,
14477
- icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.ArrowDownloadRegular, {}),
14478
- onClick: () => onDownloadMerged(culture),
14479
- children: culture
14480
- },
14481
- culture
14482
- )) })
14660
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles.mergedButtons, children: availableCultures.map((culture) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.mergedCulture, children: [
14661
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.mergedCultureLabel, children: cultureLabel(culture) }),
14662
+ /* @__PURE__ */ jsxRuntime.jsx(
14663
+ reactComponents.Tooltip,
14664
+ {
14665
+ content: t("app.localization-admin.view-merged-tooltip", [
14666
+ cultureLabel(culture)
14667
+ ]),
14668
+ relationship: "label",
14669
+ children: /* @__PURE__ */ jsxRuntime.jsx(
14670
+ reactComponents.Button,
14671
+ {
14672
+ appearance: "outline",
14673
+ icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.EyeRegular, {}),
14674
+ onClick: () => setMergedDialogCulture(culture)
14675
+ }
14676
+ )
14677
+ }
14678
+ ),
14679
+ /* @__PURE__ */ jsxRuntime.jsx(
14680
+ reactComponents.Tooltip,
14681
+ {
14682
+ content: t("app.localization-admin.download-merged-tooltip", [
14683
+ cultureLabel(culture)
14684
+ ]),
14685
+ relationship: "label",
14686
+ children: /* @__PURE__ */ jsxRuntime.jsx(
14687
+ reactComponents.Button,
14688
+ {
14689
+ appearance: "subtle",
14690
+ disabled: isDownloading,
14691
+ icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.ArrowDownloadRegular, {}),
14692
+ onClick: () => onDownloadMerged(culture)
14693
+ }
14694
+ )
14695
+ }
14696
+ )
14697
+ ] }, culture)) })
14483
14698
  ] }) }) }),
14484
14699
  /* @__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: [
14485
14700
  /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Text, { children: t("app.localization-admin.sources-description") }),
@@ -14510,7 +14725,22 @@ function LocalizationAdmin({ className }) {
14510
14725
  ] }) }) })
14511
14726
  ] });
14512
14727
  }
14513
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: reactComponents.mergeClasses("ppp-localization-admin", styles.root, className), children: content });
14728
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: reactComponents.mergeClasses("ppp-localization-admin", styles.root, className), children: [
14729
+ content,
14730
+ mergedDialogCulture !== null && /* @__PURE__ */ jsxRuntime.jsx(
14731
+ MergedSourcesDialog,
14732
+ {
14733
+ culture: mergedDialogCulture,
14734
+ label: cultureLabel(mergedDialogCulture),
14735
+ onClose: () => setMergedDialogCulture(null),
14736
+ onDownload: () => onDownloadMerged(mergedDialogCulture),
14737
+ isDownloading,
14738
+ ppp,
14739
+ t,
14740
+ styles
14741
+ }
14742
+ )
14743
+ ] });
14514
14744
  }
14515
14745
  function ConfigRow({ styles, label, value }) {
14516
14746
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -14560,6 +14790,177 @@ function SourceRow({
14560
14790
  )) }) })
14561
14791
  ] });
14562
14792
  }
14793
+ var MERGED_PAGE_SIZE = 100;
14794
+ function MergedSourcesDialog({
14795
+ culture,
14796
+ label,
14797
+ onClose,
14798
+ onDownload,
14799
+ isDownloading,
14800
+ ppp,
14801
+ t,
14802
+ styles
14803
+ }) {
14804
+ const [entries, setEntries] = react.useState(null);
14805
+ const [loadError, setLoadError] = react.useState();
14806
+ const [filter, setFilter] = react.useState("");
14807
+ const [page, setPage] = react.useState(0);
14808
+ const [sortState, setSortState] = react.useState({
14809
+ sortColumn: "key",
14810
+ sortDirection: "ascending"
14811
+ });
14812
+ react.useEffect(() => {
14813
+ let cancelled = false;
14814
+ void (async () => {
14815
+ try {
14816
+ const value = await ppp.getMergedLocalizationEntriesAsync(culture);
14817
+ if (!cancelled) {
14818
+ setEntries(value);
14819
+ setLoadError(void 0);
14820
+ }
14821
+ } catch (reason) {
14822
+ if (!cancelled) {
14823
+ setEntries(null);
14824
+ setLoadError(reason instanceof Error ? reason.message : String(reason));
14825
+ }
14826
+ }
14827
+ })();
14828
+ return () => {
14829
+ cancelled = true;
14830
+ };
14831
+ }, [ppp, culture]);
14832
+ const columns = react.useMemo(
14833
+ () => [
14834
+ reactComponents.createTableColumn({
14835
+ columnId: "key",
14836
+ compare: (a, b) => a.key.localeCompare(b.key),
14837
+ renderHeaderCell: () => t("app.localization-admin.merged-column-key"),
14838
+ renderCell: (item) => item.key
14839
+ }),
14840
+ reactComponents.createTableColumn({
14841
+ columnId: "kind",
14842
+ compare: (a, b) => Number(a.sourceKind) - Number(b.sourceKind),
14843
+ renderHeaderCell: () => t("app.localization-admin.merged-column-kind"),
14844
+ renderCell: (item) => t(`app.localization-admin.kind-${kindKeyOf(item.sourceKind)}`)
14845
+ }),
14846
+ reactComponents.createTableColumn({
14847
+ columnId: "source",
14848
+ compare: (a, b) => a.sourceDisplayName.localeCompare(b.sourceDisplayName),
14849
+ renderHeaderCell: () => t("app.localization-admin.merged-column-source"),
14850
+ renderCell: (item) => item.sourceDisplayName
14851
+ }),
14852
+ reactComponents.createTableColumn({
14853
+ columnId: "value",
14854
+ compare: (a, b) => a.value.localeCompare(b.value),
14855
+ renderHeaderCell: () => t("app.localization-admin.merged-column-value"),
14856
+ renderCell: (item) => item.value
14857
+ })
14858
+ ],
14859
+ [t]
14860
+ );
14861
+ const filtered = react.useMemo(() => {
14862
+ if (!entries) return [];
14863
+ const q = filter.trim().toLowerCase();
14864
+ const base = q ? entries.filter(
14865
+ (e) => e.key.toLowerCase().includes(q) || e.value.toLowerCase().includes(q) || e.sourceDisplayName.toLowerCase().includes(q)
14866
+ ) : entries;
14867
+ return [...base];
14868
+ }, [entries, filter]);
14869
+ const sorted = react.useMemo(() => {
14870
+ const column = columns.find((c) => c.columnId === sortState.sortColumn);
14871
+ if (!column?.compare) return filtered;
14872
+ const sign = sortState.sortDirection === "ascending" ? 1 : -1;
14873
+ return [...filtered].sort((a, b) => sign * column.compare(a, b));
14874
+ }, [filtered, columns, sortState]);
14875
+ react.useEffect(() => {
14876
+ setPage(0);
14877
+ }, [filter, sortState]);
14878
+ const pageCount = Math.max(1, Math.ceil(sorted.length / MERGED_PAGE_SIZE));
14879
+ const currentPage = Math.min(page, pageCount - 1);
14880
+ const paged = sorted.slice(currentPage * MERGED_PAGE_SIZE, currentPage * MERGED_PAGE_SIZE + MERGED_PAGE_SIZE);
14881
+ const columnSizingOptions = react.useMemo(
14882
+ () => ({
14883
+ key: { minWidth: 200, defaultWidth: 320 },
14884
+ kind: { minWidth: 110, defaultWidth: 140 },
14885
+ source: { minWidth: 200, defaultWidth: 300 },
14886
+ value: { minWidth: 200, defaultWidth: 360 }
14887
+ }),
14888
+ []
14889
+ );
14890
+ 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: [
14891
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.DialogTitle, { children: t("app.localization-admin.merged-dialog-title", [label]) }),
14892
+ /* @__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: [
14893
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles.dialogToolbar, children: [
14894
+ /* @__PURE__ */ jsxRuntime.jsx(
14895
+ reactComponents.Input,
14896
+ {
14897
+ className: styles.filterInput,
14898
+ value: filter,
14899
+ onChange: (_event, data) => setFilter(data.value),
14900
+ contentBefore: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.SearchRegular, {}),
14901
+ placeholder: t("app.localization-admin.merged-filter-placeholder")
14902
+ }
14903
+ ),
14904
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.lastReloaded, children: t("app.localization-admin.merged-count", [sorted.length, entries.length]) }),
14905
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.grow }),
14906
+ pageCount > 1 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: styles.pager, children: [
14907
+ /* @__PURE__ */ jsxRuntime.jsx(
14908
+ reactComponents.Button,
14909
+ {
14910
+ size: "small",
14911
+ appearance: "subtle",
14912
+ disabled: currentPage === 0,
14913
+ onClick: () => setPage(currentPage - 1),
14914
+ children: t("app.localization-admin.merged-prev")
14915
+ }
14916
+ ),
14917
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles.lastReloaded, children: t("app.localization-admin.merged-page", [currentPage + 1, pageCount]) }),
14918
+ /* @__PURE__ */ jsxRuntime.jsx(
14919
+ reactComponents.Button,
14920
+ {
14921
+ size: "small",
14922
+ appearance: "subtle",
14923
+ disabled: currentPage >= pageCount - 1,
14924
+ onClick: () => setPage(currentPage + 1),
14925
+ children: t("app.localization-admin.merged-next")
14926
+ }
14927
+ )
14928
+ ] })
14929
+ ] }),
14930
+ 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(
14931
+ reactComponents.DataGrid,
14932
+ {
14933
+ items: paged,
14934
+ columns,
14935
+ sortable: true,
14936
+ resizableColumns: true,
14937
+ sortState,
14938
+ onSortChange: (_event, next) => setSortState(next),
14939
+ columnSizingOptions,
14940
+ getRowId: (item) => item.key,
14941
+ size: "small",
14942
+ children: [
14943
+ /* @__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) })
14945
+ ]
14946
+ }
14947
+ ) })
14948
+ ] }) }),
14949
+ /* @__PURE__ */ jsxRuntime.jsxs(reactComponents.DialogActions, { children: [
14950
+ /* @__PURE__ */ jsxRuntime.jsx(
14951
+ reactComponents.Button,
14952
+ {
14953
+ appearance: "primary",
14954
+ icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.ArrowDownloadRegular, {}),
14955
+ disabled: isDownloading,
14956
+ onClick: onDownload,
14957
+ children: t("app.localization-admin.merged-download-button")
14958
+ }
14959
+ ),
14960
+ /* @__PURE__ */ jsxRuntime.jsx(reactComponents.Button, { appearance: "secondary", icon: /* @__PURE__ */ jsxRuntime.jsx(reactIcons.DismissRegular, {}), onClick: onClose, children: t("app.buttons.close.label") })
14961
+ ] })
14962
+ ] }) }) });
14963
+ }
14563
14964
  var useStyles13 = reactComponents.makeStyles({
14564
14965
  root: {
14565
14966
  display: "flex",