@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.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, MoreHorizontal20Regular, ChevronDoubleLeft20Regular, ChevronLeft20Regular, ChevronDoubleRight20Regular, Info12Regular, Checkmark16Regular, Dismiss16Regular, DocumentText20Regular, FolderZip20Regular, MusicNote220Regular, Video20Regular, Image20Regular, SlideText20Regular, DocumentTable20Regular, DocumentPdf20Regular } 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';
@@ -2468,6 +2468,7 @@ var ImageViewer = forwardRef(function ImageViewer2({
2468
2468
  const selectionRef = useRef(selection);
2469
2469
  selectionRef.current = selection;
2470
2470
  const prevTransformRef = useRef({ angle, zoom, pan, flipH, flipV });
2471
+ const resizingRef = useRef(false);
2471
2472
  const dragRef = useRef(null);
2472
2473
  const [isDragging, setIsDragging] = useState(false);
2473
2474
  const zoomRef = useRef(zoom);
@@ -2497,6 +2498,7 @@ var ImageViewer = forwardRef(function ImageViewer2({
2497
2498
  const cur = { angle, zoom, pan, flipH, flipV };
2498
2499
  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;
2499
2500
  if (transformChanged) {
2501
+ resizingRef.current = false;
2500
2502
  const sel = selectionRef.current;
2501
2503
  const imgEl = imgRef.current;
2502
2504
  if (sel && imgEl) {
@@ -2527,6 +2529,51 @@ var ImageViewer = forwardRef(function ImageViewer2({
2527
2529
  }
2528
2530
  prevTransformRef.current = cur;
2529
2531
  }, [angle, zoom, pan, flipH, flipV]);
2532
+ useEffect(() => {
2533
+ if (!toolbar) return;
2534
+ const container = imageContainerRef.current;
2535
+ if (!container) return;
2536
+ let prevMetrics = imgRef.current ? readImageLayoutMetrics(imgRef.current) : null;
2537
+ const observer = new ResizeObserver(() => {
2538
+ const imgEl = imgRef.current;
2539
+ if (!imgEl) return;
2540
+ const newMetrics = readImageLayoutMetrics(imgEl);
2541
+ if (!newMetrics) return;
2542
+ const sel = selectionRef.current;
2543
+ const layoutChanged = !prevMetrics || prevMetrics.layoutW !== newMetrics.layoutW || prevMetrics.layoutH !== newMetrics.layoutH || prevMetrics.offsetLeft !== newMetrics.offsetLeft || prevMetrics.offsetTop !== newMetrics.offsetTop;
2544
+ if (sel && prevMetrics && layoutChanged) {
2545
+ const natural = containerRectToImageNatural(
2546
+ sel,
2547
+ imgEl,
2548
+ angleRef.current,
2549
+ zoomRef.current,
2550
+ panRef.current,
2551
+ flipHRef.current,
2552
+ flipVRef.current,
2553
+ prevMetrics
2554
+ );
2555
+ if (natural) {
2556
+ const reprojected = imageNaturalRectToContainer(
2557
+ natural,
2558
+ imgEl,
2559
+ angleRef.current,
2560
+ zoomRef.current,
2561
+ panRef.current,
2562
+ flipHRef.current,
2563
+ flipVRef.current,
2564
+ newMetrics
2565
+ );
2566
+ if (reprojected) {
2567
+ resizingRef.current = true;
2568
+ setSelection(reprojected);
2569
+ }
2570
+ }
2571
+ }
2572
+ prevMetrics = newMetrics;
2573
+ });
2574
+ observer.observe(container);
2575
+ return () => observer.disconnect();
2576
+ }, [toolbar]);
2530
2577
  const onDirtyChangedRef = useRef(onDirtyChanged);
2531
2578
  onDirtyChangedRef.current = onDirtyChanged;
2532
2579
  useEffect(() => {
@@ -3280,7 +3327,7 @@ var ImageViewer = forwardRef(function ImageViewer2({
3280
3327
  // pointermove (which is what changes the selection during
3281
3328
  // a drag) calls setSelection — that triggers a re-render
3282
3329
  // which reads the current dragRef value.
3283
- transition: dragRef.current ? "none" : "left 120ms ease-out, top 120ms ease-out, width 120ms ease-out, height 120ms ease-out"
3330
+ transition: dragRef.current || resizingRef.current ? "none" : "left 120ms ease-out, top 120ms ease-out, width 120ms ease-out, height 120ms ease-out"
3284
3331
  },
3285
3332
  children: [
3286
3333
  showThirds && /* @__PURE__ */ jsxs("div", { className: styles.thirdsGuide, "aria-hidden": "true", children: [
@@ -3441,6 +3488,21 @@ function useImageSrc(source) {
3441
3488
  return source.value ? `data:${resolveMimeType(source)};base64,${source.value}` : null;
3442
3489
  return bytesUrlRef.current;
3443
3490
  }
3491
+ function readImageLayoutMetrics(imageEl) {
3492
+ const layoutW = imageEl.offsetWidth;
3493
+ const layoutH = imageEl.offsetHeight;
3494
+ const naturalW = imageEl.naturalWidth;
3495
+ const naturalH = imageEl.naturalHeight;
3496
+ if (layoutW === 0 || layoutH === 0 || naturalW === 0 || naturalH === 0) return null;
3497
+ return {
3498
+ layoutW,
3499
+ layoutH,
3500
+ offsetLeft: imageEl.offsetLeft,
3501
+ offsetTop: imageEl.offsetTop,
3502
+ naturalW,
3503
+ naturalH
3504
+ };
3505
+ }
3444
3506
  function getDisplayedImageBounds(imageEl, angle, zoom, pan, flipH, flipV) {
3445
3507
  const layoutW = imageEl.offsetWidth;
3446
3508
  const layoutH = imageEl.offsetHeight;
@@ -3483,15 +3545,13 @@ function getDisplayedImageBounds(imageEl, angle, zoom, pan, flipH, flipV) {
3483
3545
  }
3484
3546
  return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3485
3547
  }
3486
- function containerRectToImageNatural(rect, imageEl, angle, zoom, pan, flipH, flipV) {
3487
- const layoutW = imageEl.offsetWidth;
3488
- const layoutH = imageEl.offsetHeight;
3489
- const naturalW = imageEl.naturalWidth;
3490
- const naturalH = imageEl.naturalHeight;
3491
- if (layoutW === 0 || layoutH === 0 || naturalW === 0 || naturalH === 0) return null;
3548
+ function containerRectToImageNatural(rect, imageEl, angle, zoom, pan, flipH, flipV, metricsOverride) {
3549
+ const m = metricsOverride ?? readImageLayoutMetrics(imageEl);
3550
+ if (!m) return null;
3551
+ const { layoutW, layoutH, naturalW, naturalH } = m;
3492
3552
  const containScale = Math.min(layoutW / naturalW, layoutH / naturalH);
3493
- const imgCenterX = imageEl.offsetLeft + layoutW / 2;
3494
- const imgCenterY = imageEl.offsetTop + layoutH / 2;
3553
+ const imgCenterX = m.offsetLeft + layoutW / 2;
3554
+ const imgCenterY = m.offsetTop + layoutH / 2;
3495
3555
  const sx = zoom * (flipH ? -1 : 1);
3496
3556
  const sy = zoom * (flipV ? -1 : 1);
3497
3557
  if (sx === 0 || sy === 0) return null;
@@ -3524,15 +3584,13 @@ function containerRectToImageNatural(rect, imageEl, angle, zoom, pan, flipH, fli
3524
3584
  }
3525
3585
  return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
3526
3586
  }
3527
- function imageNaturalRectToContainer(rect, imageEl, angle, zoom, pan, flipH, flipV) {
3528
- const layoutW = imageEl.offsetWidth;
3529
- const layoutH = imageEl.offsetHeight;
3530
- const naturalW = imageEl.naturalWidth;
3531
- const naturalH = imageEl.naturalHeight;
3532
- if (layoutW === 0 || layoutH === 0 || naturalW === 0 || naturalH === 0) return null;
3587
+ function imageNaturalRectToContainer(rect, imageEl, angle, zoom, pan, flipH, flipV, metricsOverride) {
3588
+ const m = metricsOverride ?? readImageLayoutMetrics(imageEl);
3589
+ if (!m) return null;
3590
+ const { layoutW, layoutH, naturalW, naturalH } = m;
3533
3591
  const containScale = Math.min(layoutW / naturalW, layoutH / naturalH);
3534
- const imgCenterX = imageEl.offsetLeft + layoutW / 2;
3535
- const imgCenterY = imageEl.offsetTop + layoutH / 2;
3592
+ const imgCenterX = m.offsetLeft + layoutW / 2;
3593
+ const imgCenterY = m.offsetTop + layoutH / 2;
3536
3594
  const sx = zoom * (flipH ? -1 : 1);
3537
3595
  const sy = zoom * (flipV ? -1 : 1);
3538
3596
  const rad = angle * Math.PI / 180;
@@ -4410,6 +4468,69 @@ function ZipViewer({
4410
4468
  }
4411
4469
  );
4412
4470
  }
4471
+ function ZipEntryIcon({ fileName, className }) {
4472
+ switch (getFileExtension(fileName)) {
4473
+ case "pdf":
4474
+ return /* @__PURE__ */ jsx(DocumentPdf20Regular, { className });
4475
+ case "xls":
4476
+ case "xlsx":
4477
+ case "csv":
4478
+ case "ods":
4479
+ return /* @__PURE__ */ jsx(DocumentTable20Regular, { className });
4480
+ case "ppt":
4481
+ case "pptx":
4482
+ case "odp":
4483
+ return /* @__PURE__ */ jsx(SlideText20Regular, { className });
4484
+ case "png":
4485
+ case "jpg":
4486
+ case "jpeg":
4487
+ case "gif":
4488
+ case "bmp":
4489
+ case "webp":
4490
+ case "svg":
4491
+ case "ico":
4492
+ case "tif":
4493
+ case "tiff":
4494
+ return /* @__PURE__ */ jsx(Image20Regular, { className });
4495
+ case "mp4":
4496
+ case "mov":
4497
+ case "avi":
4498
+ case "mkv":
4499
+ case "webm":
4500
+ case "m4v":
4501
+ return /* @__PURE__ */ jsx(Video20Regular, { className });
4502
+ case "mp3":
4503
+ case "wav":
4504
+ case "ogg":
4505
+ case "flac":
4506
+ case "m4a":
4507
+ case "aac":
4508
+ return /* @__PURE__ */ jsx(MusicNote220Regular, { className });
4509
+ case "zip":
4510
+ case "7z":
4511
+ case "rar":
4512
+ case "tar":
4513
+ case "gz":
4514
+ return /* @__PURE__ */ jsx(FolderZip20Regular, { className });
4515
+ case "txt":
4516
+ case "md":
4517
+ case "markdown":
4518
+ case "log":
4519
+ case "json":
4520
+ case "xml":
4521
+ case "yaml":
4522
+ case "yml":
4523
+ case "html":
4524
+ case "htm":
4525
+ case "js":
4526
+ case "ts":
4527
+ case "cs":
4528
+ case "css":
4529
+ return /* @__PURE__ */ jsx(DocumentText20Regular, { className });
4530
+ default:
4531
+ return /* @__PURE__ */ jsx(Document20Regular, { className });
4532
+ }
4533
+ }
4413
4534
  function ZipTreeNode({ node, depth, expanded, selected, onToggle, onSelect }) {
4414
4535
  const styles = useFileViewerStyles();
4415
4536
  const isExpanded = expanded.has(node.fullPath);
@@ -4468,7 +4589,7 @@ function ZipTreeNode({ node, depth, expanded, selected, onToggle, onSelect }) {
4468
4589
  style: indent,
4469
4590
  children: [
4470
4591
  /* @__PURE__ */ jsx("span", { className: styles.zipChevronSpacer }),
4471
- /* @__PURE__ */ jsx(Document20Regular, { className: styles.zipIcon }),
4592
+ /* @__PURE__ */ jsx(ZipEntryIcon, { fileName: node.name, className: styles.zipIcon }),
4472
4593
  /* @__PURE__ */ jsx("span", { className: styles.zipName, children: node.name })
4473
4594
  ]
4474
4595
  }
@@ -5113,11 +5234,13 @@ var useFileViewerStyles = makeStyles({
5113
5234
  outlineOffset: "-1px"
5114
5235
  }
5115
5236
  },
5237
+ // Selection style ported from the Blazor zip viewer: a subtle accent tint over
5238
+ // the row (text stays its normal color), intensifying slightly on hover so the
5239
+ // neutral row-hover doesn't wash out the selection.
5116
5240
  zipRowSelected: {
5117
- backgroundColor: tokens.colorBrandBackground2,
5118
- color: tokens.colorBrandForeground2,
5241
+ backgroundColor: `color-mix(in srgb, ${tokens.colorBrandBackground} 12%, transparent)`,
5119
5242
  ":hover": {
5120
- backgroundColor: tokens.colorBrandBackground2Hover
5243
+ backgroundColor: `color-mix(in srgb, ${tokens.colorBrandBackground} 18%, transparent)`
5121
5244
  }
5122
5245
  },
5123
5246
  zipChevron: {
@@ -8892,7 +9015,13 @@ function MainGridImpl({
8892
9015
  (row) => row.properties?.[columnName],
8893
9016
  {
8894
9017
  id: columnName,
8895
- header: () => resolvedColumn.displayName,
9018
+ // Headers normally show the server-resolved Dataverse display name, which bypasses the
9019
+ // client `t()` — so the ?ppp-show-keys aid couldn't reach them. Consult key-mode here too:
9020
+ // when it's on, show the column's localization key (the one a consumer would override),
9021
+ // keyed by the column's OWN owning table (`resolvedColumn.tableName` — the related table
9022
+ // for a linked-entity column, not the grid's primary table). For linked columns the name
9023
+ // is a dotted alias path, so take the segment after the last dot as the real column name.
9024
+ header: () => isKeyModeEnabled() ? `tables.${resolvedColumn.tableName}.columns.${columnName.split(".").pop() ?? columnName}.label` : resolvedColumn.displayName,
8896
9025
  cell: (cellCtx) => {
8897
9026
  const rowRecord = cellCtx.row.original;
8898
9027
  const rowId = rowRecord.id;
@@ -11656,15 +11785,11 @@ function ManyToManyLookupEdit({
11656
11785
  return relatedPrimaryName;
11657
11786
  }, [resolvedColumns, relatedPrimaryName]);
11658
11787
  const lastSeededRecordIdRef = useRef(void 0);
11659
- const lastSeededViewIdRef = useRef(void 0);
11660
11788
  useEffect(() => {
11661
11789
  if (initialLoad.status !== QueryStatus.Success || !initialLoad.data) return;
11662
11790
  if (!primaryNameColumn || !parentRecord) return;
11663
- if (lastSeededRecordIdRef.current === parentRefId && lastSeededViewIdRef.current === selectedViewId) {
11664
- return;
11665
- }
11791
+ if (lastSeededRecordIdRef.current === parentRefId) return;
11666
11792
  lastSeededRecordIdRef.current = parentRefId;
11667
- lastSeededViewIdRef.current = selectedViewId;
11668
11793
  const seeded = initialLoad.data.tableRecords.map((r) => ({
11669
11794
  id: r.id,
11670
11795
  displayName: resolveDisplayName(r, primaryNameColumn) ?? "",
@@ -11673,7 +11798,7 @@ function ManyToManyLookupEdit({
11673
11798
  setInitialSelection(seeded);
11674
11799
  setPendingAdds([]);
11675
11800
  setPendingRemoveIds([]);
11676
- }, [initialLoad.status, initialLoad.data, parentRecord, primaryNameColumn, selectedViewId]);
11801
+ }, [initialLoad.status, initialLoad.data, parentRecord, primaryNameColumn, parentRefId]);
11677
11802
  const [query, setQuery] = useState("");
11678
11803
  const [debouncedQuery, setDebouncedQuery] = useState("");
11679
11804
  const [searchResults, setSearchResults] = useState([]);
@@ -11785,7 +11910,6 @@ function ManyToManyLookupEdit({
11785
11910
  setPendingAdds([]);
11786
11911
  setPendingRemoveIds([]);
11787
11912
  lastSeededRecordIdRef.current = void 0;
11788
- lastSeededViewIdRef.current = void 0;
11789
11913
  },
11790
11914
  // Cancel/Discard: just drop the pending adds/removes; the
11791
11915
  // seed-guard refs stay so the initial selection (already loaded
@@ -13440,27 +13564,57 @@ function NavMenu({ children, width, className, style }) {
13440
13564
  useNavSelectedStyles();
13441
13565
  const location = useLocation();
13442
13566
  const navigate = useNavigate();
13443
- const { allRoutes, defaultOpen } = useMemo(
13567
+ const { allRoutes, groups, defaultOpenIndexes } = useMemo(
13444
13568
  () => collectNavTree(children),
13445
13569
  [children]
13446
13570
  );
13447
- const selectedValue = useMemo(
13571
+ const selectedRoute = useMemo(
13448
13572
  () => bestRouteMatch(allRoutes, location.pathname),
13449
13573
  [allRoutes, location.pathname]
13450
13574
  );
13575
+ const [openIndexes, setOpenIndexes] = useState(
13576
+ () => /* @__PURE__ */ new Set([
13577
+ ...defaultOpenIndexes,
13578
+ // Seed the active route's group too, so it's open on first paint
13579
+ // (the effect below only fires after mount).
13580
+ ...selectedRoute && selectedRoute.groupIndex >= 0 ? [selectedRoute.groupIndex] : []
13581
+ ])
13582
+ );
13583
+ const selectedGroupIndex = selectedRoute?.groupIndex ?? -1;
13584
+ useEffect(() => {
13585
+ if (selectedGroupIndex < 0) return;
13586
+ setOpenIndexes(
13587
+ (prev) => prev.has(selectedGroupIndex) ? prev : new Set(prev).add(selectedGroupIndex)
13588
+ );
13589
+ }, [selectedGroupIndex]);
13590
+ const openCategories = useMemo(
13591
+ () => groups.filter((_, index) => openIndexes.has(index)),
13592
+ [groups, openIndexes]
13593
+ );
13451
13594
  const handleSelect = (_, data) => {
13452
13595
  const value = data.value;
13453
13596
  if (typeof value === "string" && value.startsWith("/")) {
13454
13597
  navigate(value);
13455
13598
  }
13456
13599
  };
13600
+ const handleCategoryToggle = (_, data) => {
13601
+ const index = groups.indexOf(data.value);
13602
+ if (index < 0) return;
13603
+ setOpenIndexes((prev) => {
13604
+ const next = new Set(prev);
13605
+ if (next.has(index)) next.delete(index);
13606
+ else next.add(index);
13607
+ return next;
13608
+ });
13609
+ };
13457
13610
  const navStyle = width ? { ...style, minWidth: width, maxWidth: width } : style;
13458
13611
  return /* @__PURE__ */ jsx(
13459
13612
  Nav,
13460
13613
  {
13461
- selectedValue: selectedValue ?? "",
13614
+ selectedValue: selectedRoute?.to ?? "",
13462
13615
  onNavItemSelect: handleSelect,
13463
- defaultOpenCategories: defaultOpen,
13616
+ openCategories,
13617
+ onNavCategoryItemToggle: handleCategoryToggle,
13464
13618
  className: mergeClasses("ppp-nav-menu", styles.nav, className),
13465
13619
  style: navStyle,
13466
13620
  children
@@ -13490,42 +13644,46 @@ function NavItem({ to, icon, children }) {
13490
13644
  }
13491
13645
  function collectNavTree(children) {
13492
13646
  const allRoutes = [];
13493
- const defaultOpen = [];
13494
- const visit = (nodes) => {
13647
+ const groups = [];
13648
+ const defaultOpenIndexes = [];
13649
+ const visit = (nodes, groupIndex) => {
13495
13650
  Children.forEach(nodes, (child) => {
13496
13651
  if (!isValidElement(child)) return;
13497
13652
  if (child.type === NavGroup) {
13498
13653
  const props = child.props;
13499
13654
  const value = props.value ?? `group:${props.label}`;
13500
- if (props.defaultExpanded) defaultOpen.push(value);
13501
- visit(props.children);
13655
+ const index = groups.length;
13656
+ groups.push(value);
13657
+ if (props.defaultExpanded) defaultOpenIndexes.push(index);
13658
+ visit(props.children, index);
13502
13659
  return;
13503
13660
  }
13504
13661
  if (child.type === NavItem) {
13505
13662
  const props = child.props;
13506
13663
  allRoutes.push({
13507
13664
  to: props.to,
13508
- match: props.match ?? (props.to === "/" ? "exact" : "prefix")
13665
+ match: props.match ?? (props.to === "/" ? "exact" : "prefix"),
13666
+ groupIndex
13509
13667
  });
13510
13668
  return;
13511
13669
  }
13512
13670
  const wrapperChildren = child.props.children;
13513
- if (wrapperChildren !== void 0) visit(wrapperChildren);
13671
+ if (wrapperChildren !== void 0) visit(wrapperChildren, groupIndex);
13514
13672
  });
13515
13673
  };
13516
- visit(children);
13517
- return { allRoutes, defaultOpen };
13674
+ visit(children, -1);
13675
+ return { allRoutes, groups, defaultOpenIndexes };
13518
13676
  }
13519
13677
  function bestRouteMatch(routes, pathname) {
13520
13678
  const normalize = (p) => p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
13521
13679
  const target = normalize(pathname);
13522
13680
  let best;
13523
13681
  let bestLen = -1;
13524
- for (const { to, match } of routes) {
13525
- const route = normalize(to);
13526
- const matches = route === target || match === "prefix" && (route === "/" || target.startsWith(route + "/"));
13682
+ for (const candidate of routes) {
13683
+ const route = normalize(candidate.to);
13684
+ const matches = route === target || candidate.match === "prefix" && (route === "/" || target.startsWith(route + "/"));
13527
13685
  if (matches && route.length > bestLen) {
13528
- best = to;
13686
+ best = candidate;
13529
13687
  bestLen = route.length;
13530
13688
  }
13531
13689
  }
@@ -14268,8 +14426,28 @@ var useStyles12 = makeStyles({
14268
14426
  mergedButtons: {
14269
14427
  display: "flex",
14270
14428
  flexWrap: "wrap",
14271
- gap: tokens.spacingHorizontalS
14429
+ gap: tokens.spacingHorizontalL
14430
+ },
14431
+ // One culture group: language label + inspect button + download button, kept together.
14432
+ mergedCulture: {
14433
+ display: "flex",
14434
+ alignItems: "center",
14435
+ gap: tokens.spacingHorizontalXS
14436
+ },
14437
+ mergedCultureLabel: { fontSize: tokens.fontSizeBase200 },
14438
+ dialogSurface: { maxWidth: "1100px", width: "90vw" },
14439
+ dialogToolbar: {
14440
+ display: "flex",
14441
+ alignItems: "center",
14442
+ gap: tokens.spacingHorizontalM,
14443
+ flexWrap: "wrap",
14444
+ marginBottom: tokens.spacingVerticalS
14272
14445
  },
14446
+ // Scroll viewport for the merged grid so a tall result set scrolls inside the dialog.
14447
+ dialogGrid: { maxHeight: "60vh", overflowY: "auto", overflowX: "auto" },
14448
+ filterInput: { minWidth: "260px" },
14449
+ pager: { display: "flex", alignItems: "center", gap: tokens.spacingHorizontalS },
14450
+ grow: { flex: 1 },
14273
14451
  sectionBody: {
14274
14452
  display: "flex",
14275
14453
  flexDirection: "column",
@@ -14288,6 +14466,15 @@ function downloadBytes(fileName, data, contentType = "application/json") {
14288
14466
  anchor.click();
14289
14467
  URL.revokeObjectURL(url);
14290
14468
  }
14469
+ var kindKeyOf = (kind) => KIND_KEY[Number(kind)] ?? "tablemetadata";
14470
+ function cultureLabel(culture) {
14471
+ try {
14472
+ const name = new Intl.DisplayNames(void 0, { type: "language" }).of(culture);
14473
+ return name && name.toLowerCase() !== culture.toLowerCase() ? `${name} (${culture})` : culture;
14474
+ } catch {
14475
+ return culture;
14476
+ }
14477
+ }
14291
14478
  function LocalizationAdmin({ className }) {
14292
14479
  const styles = useStyles12();
14293
14480
  const t = useT();
@@ -14297,6 +14484,7 @@ function LocalizationAdmin({ className }) {
14297
14484
  const [isReloading, setIsReloading] = useState(false);
14298
14485
  const [isDownloading, setIsDownloading] = useState(false);
14299
14486
  const [message, setMessage] = useState();
14487
+ const [mergedDialogCulture, setMergedDialogCulture] = useState(null);
14300
14488
  const loadOverview = useCallback(async () => {
14301
14489
  try {
14302
14490
  const value = await ppp.getLocalizationOverviewAsync();
@@ -14454,17 +14642,44 @@ function LocalizationAdmin({ className }) {
14454
14642
  /* @__PURE__ */ jsx(LocalizationTranslator, {}),
14455
14643
  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: [
14456
14644
  /* @__PURE__ */ jsx(Text, { children: t("app.localization-admin.download-merged-description") }),
14457
- /* @__PURE__ */ jsx("div", { className: styles.mergedButtons, children: availableCultures.map((culture) => /* @__PURE__ */ jsx(
14458
- Button,
14459
- {
14460
- appearance: "outline",
14461
- disabled: isDownloading,
14462
- icon: /* @__PURE__ */ jsx(ArrowDownloadRegular, {}),
14463
- onClick: () => onDownloadMerged(culture),
14464
- children: culture
14465
- },
14466
- culture
14467
- )) })
14645
+ /* @__PURE__ */ jsx("div", { className: styles.mergedButtons, children: availableCultures.map((culture) => /* @__PURE__ */ jsxs("div", { className: styles.mergedCulture, children: [
14646
+ /* @__PURE__ */ jsx("span", { className: styles.mergedCultureLabel, children: cultureLabel(culture) }),
14647
+ /* @__PURE__ */ jsx(
14648
+ Tooltip,
14649
+ {
14650
+ content: t("app.localization-admin.view-merged-tooltip", [
14651
+ cultureLabel(culture)
14652
+ ]),
14653
+ relationship: "label",
14654
+ children: /* @__PURE__ */ jsx(
14655
+ Button,
14656
+ {
14657
+ appearance: "outline",
14658
+ icon: /* @__PURE__ */ jsx(EyeRegular, {}),
14659
+ onClick: () => setMergedDialogCulture(culture)
14660
+ }
14661
+ )
14662
+ }
14663
+ ),
14664
+ /* @__PURE__ */ jsx(
14665
+ Tooltip,
14666
+ {
14667
+ content: t("app.localization-admin.download-merged-tooltip", [
14668
+ cultureLabel(culture)
14669
+ ]),
14670
+ relationship: "label",
14671
+ children: /* @__PURE__ */ jsx(
14672
+ Button,
14673
+ {
14674
+ appearance: "subtle",
14675
+ disabled: isDownloading,
14676
+ icon: /* @__PURE__ */ jsx(ArrowDownloadRegular, {}),
14677
+ onClick: () => onDownloadMerged(culture)
14678
+ }
14679
+ )
14680
+ }
14681
+ )
14682
+ ] }, culture)) })
14468
14683
  ] }) }) }),
14469
14684
  /* @__PURE__ */ jsx(Section, { headerText: t("app.localization-admin.sources-title"), borderVisible: true, children: /* @__PURE__ */ jsx(SectionColumn, { children: /* @__PURE__ */ jsxs("div", { className: styles.sectionBody, children: [
14470
14685
  /* @__PURE__ */ jsx(Text, { children: t("app.localization-admin.sources-description") }),
@@ -14495,7 +14710,22 @@ function LocalizationAdmin({ className }) {
14495
14710
  ] }) }) })
14496
14711
  ] });
14497
14712
  }
14498
- return /* @__PURE__ */ jsx("div", { className: mergeClasses("ppp-localization-admin", styles.root, className), children: content });
14713
+ return /* @__PURE__ */ jsxs("div", { className: mergeClasses("ppp-localization-admin", styles.root, className), children: [
14714
+ content,
14715
+ mergedDialogCulture !== null && /* @__PURE__ */ jsx(
14716
+ MergedSourcesDialog,
14717
+ {
14718
+ culture: mergedDialogCulture,
14719
+ label: cultureLabel(mergedDialogCulture),
14720
+ onClose: () => setMergedDialogCulture(null),
14721
+ onDownload: () => onDownloadMerged(mergedDialogCulture),
14722
+ isDownloading,
14723
+ ppp,
14724
+ t,
14725
+ styles
14726
+ }
14727
+ )
14728
+ ] });
14499
14729
  }
14500
14730
  function ConfigRow({ styles, label, value }) {
14501
14731
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -14545,6 +14775,177 @@ function SourceRow({
14545
14775
  )) }) })
14546
14776
  ] });
14547
14777
  }
14778
+ var MERGED_PAGE_SIZE = 100;
14779
+ function MergedSourcesDialog({
14780
+ culture,
14781
+ label,
14782
+ onClose,
14783
+ onDownload,
14784
+ isDownloading,
14785
+ ppp,
14786
+ t,
14787
+ styles
14788
+ }) {
14789
+ const [entries, setEntries] = useState(null);
14790
+ const [loadError, setLoadError] = useState();
14791
+ const [filter, setFilter] = useState("");
14792
+ const [page, setPage] = useState(0);
14793
+ const [sortState, setSortState] = useState({
14794
+ sortColumn: "key",
14795
+ sortDirection: "ascending"
14796
+ });
14797
+ useEffect(() => {
14798
+ let cancelled = false;
14799
+ void (async () => {
14800
+ try {
14801
+ const value = await ppp.getMergedLocalizationEntriesAsync(culture);
14802
+ if (!cancelled) {
14803
+ setEntries(value);
14804
+ setLoadError(void 0);
14805
+ }
14806
+ } catch (reason) {
14807
+ if (!cancelled) {
14808
+ setEntries(null);
14809
+ setLoadError(reason instanceof Error ? reason.message : String(reason));
14810
+ }
14811
+ }
14812
+ })();
14813
+ return () => {
14814
+ cancelled = true;
14815
+ };
14816
+ }, [ppp, culture]);
14817
+ const columns = useMemo(
14818
+ () => [
14819
+ createTableColumn({
14820
+ columnId: "key",
14821
+ compare: (a, b) => a.key.localeCompare(b.key),
14822
+ renderHeaderCell: () => t("app.localization-admin.merged-column-key"),
14823
+ renderCell: (item) => item.key
14824
+ }),
14825
+ createTableColumn({
14826
+ columnId: "kind",
14827
+ compare: (a, b) => Number(a.sourceKind) - Number(b.sourceKind),
14828
+ renderHeaderCell: () => t("app.localization-admin.merged-column-kind"),
14829
+ renderCell: (item) => t(`app.localization-admin.kind-${kindKeyOf(item.sourceKind)}`)
14830
+ }),
14831
+ createTableColumn({
14832
+ columnId: "source",
14833
+ compare: (a, b) => a.sourceDisplayName.localeCompare(b.sourceDisplayName),
14834
+ renderHeaderCell: () => t("app.localization-admin.merged-column-source"),
14835
+ renderCell: (item) => item.sourceDisplayName
14836
+ }),
14837
+ createTableColumn({
14838
+ columnId: "value",
14839
+ compare: (a, b) => a.value.localeCompare(b.value),
14840
+ renderHeaderCell: () => t("app.localization-admin.merged-column-value"),
14841
+ renderCell: (item) => item.value
14842
+ })
14843
+ ],
14844
+ [t]
14845
+ );
14846
+ const filtered = useMemo(() => {
14847
+ if (!entries) return [];
14848
+ const q = filter.trim().toLowerCase();
14849
+ const base = q ? entries.filter(
14850
+ (e) => e.key.toLowerCase().includes(q) || e.value.toLowerCase().includes(q) || e.sourceDisplayName.toLowerCase().includes(q)
14851
+ ) : entries;
14852
+ return [...base];
14853
+ }, [entries, filter]);
14854
+ const sorted = useMemo(() => {
14855
+ const column = columns.find((c) => c.columnId === sortState.sortColumn);
14856
+ if (!column?.compare) return filtered;
14857
+ const sign = sortState.sortDirection === "ascending" ? 1 : -1;
14858
+ return [...filtered].sort((a, b) => sign * column.compare(a, b));
14859
+ }, [filtered, columns, sortState]);
14860
+ useEffect(() => {
14861
+ setPage(0);
14862
+ }, [filter, sortState]);
14863
+ const pageCount = Math.max(1, Math.ceil(sorted.length / MERGED_PAGE_SIZE));
14864
+ const currentPage = Math.min(page, pageCount - 1);
14865
+ const paged = sorted.slice(currentPage * MERGED_PAGE_SIZE, currentPage * MERGED_PAGE_SIZE + MERGED_PAGE_SIZE);
14866
+ const columnSizingOptions = useMemo(
14867
+ () => ({
14868
+ key: { minWidth: 200, defaultWidth: 320 },
14869
+ kind: { minWidth: 110, defaultWidth: 140 },
14870
+ source: { minWidth: 200, defaultWidth: 300 },
14871
+ value: { minWidth: 200, defaultWidth: 360 }
14872
+ }),
14873
+ []
14874
+ );
14875
+ return /* @__PURE__ */ jsx(Dialog, { open: true, onOpenChange: (_event, data) => !data.open && onClose(), children: /* @__PURE__ */ jsx(DialogSurface, { className: styles.dialogSurface, children: /* @__PURE__ */ jsxs(DialogBody, { children: [
14876
+ /* @__PURE__ */ jsx(DialogTitle, { children: t("app.localization-admin.merged-dialog-title", [label]) }),
14877
+ /* @__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: [
14878
+ /* @__PURE__ */ jsxs("div", { className: styles.dialogToolbar, children: [
14879
+ /* @__PURE__ */ jsx(
14880
+ Input,
14881
+ {
14882
+ className: styles.filterInput,
14883
+ value: filter,
14884
+ onChange: (_event, data) => setFilter(data.value),
14885
+ contentBefore: /* @__PURE__ */ jsx(SearchRegular, {}),
14886
+ placeholder: t("app.localization-admin.merged-filter-placeholder")
14887
+ }
14888
+ ),
14889
+ /* @__PURE__ */ jsx("span", { className: styles.lastReloaded, children: t("app.localization-admin.merged-count", [sorted.length, entries.length]) }),
14890
+ /* @__PURE__ */ jsx("span", { className: styles.grow }),
14891
+ pageCount > 1 && /* @__PURE__ */ jsxs("span", { className: styles.pager, children: [
14892
+ /* @__PURE__ */ jsx(
14893
+ Button,
14894
+ {
14895
+ size: "small",
14896
+ appearance: "subtle",
14897
+ disabled: currentPage === 0,
14898
+ onClick: () => setPage(currentPage - 1),
14899
+ children: t("app.localization-admin.merged-prev")
14900
+ }
14901
+ ),
14902
+ /* @__PURE__ */ jsx("span", { className: styles.lastReloaded, children: t("app.localization-admin.merged-page", [currentPage + 1, pageCount]) }),
14903
+ /* @__PURE__ */ jsx(
14904
+ Button,
14905
+ {
14906
+ size: "small",
14907
+ appearance: "subtle",
14908
+ disabled: currentPage >= pageCount - 1,
14909
+ onClick: () => setPage(currentPage + 1),
14910
+ children: t("app.localization-admin.merged-next")
14911
+ }
14912
+ )
14913
+ ] })
14914
+ ] }),
14915
+ 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(
14916
+ DataGrid,
14917
+ {
14918
+ items: paged,
14919
+ columns,
14920
+ sortable: true,
14921
+ resizableColumns: true,
14922
+ sortState,
14923
+ onSortChange: (_event, next) => setSortState(next),
14924
+ columnSizingOptions,
14925
+ getRowId: (item) => item.key,
14926
+ size: "small",
14927
+ children: [
14928
+ /* @__PURE__ */ jsx(DataGridHeader, { children: /* @__PURE__ */ jsx(DataGridRow, { children: ({ renderHeaderCell }) => /* @__PURE__ */ jsx(DataGridHeaderCell, { children: renderHeaderCell() }) }) }),
14929
+ /* @__PURE__ */ jsx(DataGridBody, { children: ({ item, rowId }) => /* @__PURE__ */ jsx(DataGridRow, { children: ({ renderCell }) => /* @__PURE__ */ jsx(DataGridCell, { children: renderCell(item) }) }, rowId) })
14930
+ ]
14931
+ }
14932
+ ) })
14933
+ ] }) }),
14934
+ /* @__PURE__ */ jsxs(DialogActions, { children: [
14935
+ /* @__PURE__ */ jsx(
14936
+ Button,
14937
+ {
14938
+ appearance: "primary",
14939
+ icon: /* @__PURE__ */ jsx(ArrowDownloadRegular, {}),
14940
+ disabled: isDownloading,
14941
+ onClick: onDownload,
14942
+ children: t("app.localization-admin.merged-download-button")
14943
+ }
14944
+ ),
14945
+ /* @__PURE__ */ jsx(Button, { appearance: "secondary", icon: /* @__PURE__ */ jsx(DismissRegular, {}), onClick: onClose, children: t("app.buttons.close.label") })
14946
+ ] })
14947
+ ] }) }) });
14948
+ }
14548
14949
  var useStyles13 = makeStyles({
14549
14950
  root: {
14550
14951
  display: "flex",