@ifc-lite/viewer 1.25.2 → 1.27.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/.turbo/turbo-build.log +40 -30
- package/CHANGELOG.md +110 -0
- package/dist/assets/{basketViewActivator-CTgyKI3U.js → basketViewActivator-B3CdrLsb.js} +7 -7
- package/dist/assets/{bcf-7jQby1qi.js → bcf-QeHK_Aud.js} +5 -5
- package/dist/assets/{browser-DXS29_v9.js → browser-BIoDDfBW.js} +1 -1
- package/dist/assets/{cesium-BoVuJvTC.js → cesium-CzZn5yVA.js} +319 -319
- package/dist/assets/{deflate-Cfp9t1Df.js → deflate-B-d0SYQM.js} +1 -1
- package/dist/assets/exceljs.min-DsuzKYnj.js +29 -0
- package/dist/assets/{exporters-DfSvJPi4.js → exporters-B4LbZFeT.js} +1434 -1179
- package/dist/assets/geometry.worker-BdH-E6NB.js +1 -0
- package/dist/assets/{geotiff-xZoE8BkO.js → geotiff-CrVtDRFq.js} +10 -10
- package/dist/assets/html2canvas.esm-Ge7aVWlp.js +5 -0
- package/dist/assets/{ids-Cu73hD0Y.js → ids-DjsGFN10.js} +21 -21
- package/dist/assets/ifc-lite_bg-DsYUIHm3.wasm +0 -0
- package/dist/assets/{index-WSbA5iy6.js → index-COYokSKc.js} +44122 -38782
- package/dist/assets/index-ajK6D32J.css +1 -0
- package/dist/assets/index.es-CY202jA3.js +6866 -0
- package/dist/assets/{jpeg-DhwFEbqb.js → jpeg-D4wOkf5h.js} +1 -1
- package/dist/assets/jspdf.es.min-DIGb9BHN.js +19571 -0
- package/dist/assets/jspdf.plugin.autotable-BBLUVd7n.js +2 -0
- package/dist/assets/{lerc-Dz6BXOVb.js → lerc-DmW0_tgf.js} +1 -1
- package/dist/assets/{lzw-C9z0fG2o.js → lzw-oWetY-d6.js} +1 -1
- package/dist/assets/{maplibre-gl-Do6O5tDc.js → maplibre-gl-BF3Z0idw.js} +1 -1
- package/dist/assets/{native-bridge-RvDmzO-2.js → native-bridge-BX8_tHXE.js} +1 -1
- package/dist/assets/{packbits-jfwifz7C.js → packbits-F8Nkp4NY.js} +1 -1
- package/dist/assets/{pako.esm-Cram60i4.js → pako.esm-n3Pgozwg.js} +1 -1
- package/dist/assets/{parser.worker-C594dWxH.js → parser.worker-D591Zu_-.js} +3 -3
- package/dist/assets/pdf-Dsh3HPZB.js +135 -0
- package/dist/assets/raw-D9iw0tmc.js +1 -0
- package/dist/assets/{sandbox-DDSZ7rek.js → sandbox-BAC3a-eN.js} +4235 -2716
- package/dist/assets/server-client-Cjwnm7il.js +706 -0
- package/dist/assets/{webimage-XFHVyVtC.js → webimage-BLV1dgmd.js} +1 -1
- package/dist/assets/xlsx-Bc2HTrjC.js +142 -0
- package/dist/assets/{zip-BJqVbRkU.js → zip-DFgP-l20.js} +1 -1
- package/dist/assets/{zstd-3q5qcl5V.js → zstd-C_1HxVrA.js} +1 -1
- package/dist/index.html +8 -8
- package/package.json +13 -9
- package/src/components/extensions/FlavorDialog.tsx +18 -2
- package/src/components/extensions/FlavorListView.tsx +12 -3
- package/src/components/mcp/PlaygroundChat.tsx +1 -0
- package/src/components/mcp/data.ts +6 -0
- package/src/components/mcp/playground-dispatcher.ts +277 -0
- package/src/components/mcp/types.ts +2 -1
- package/src/components/ui/combo-input.tsx +163 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/ClashBcfExportDialog.tsx +271 -0
- package/src/components/viewer/ClashPanel.tsx +370 -0
- package/src/components/viewer/ClashSettingsDialog.tsx +407 -0
- package/src/components/viewer/CommandPalette.tsx +14 -15
- package/src/components/viewer/MainToolbar.tsx +155 -175
- package/src/components/viewer/PropertiesPanel.tsx +13 -6
- package/src/components/viewer/SearchInline.tsx +62 -2
- package/src/components/viewer/SearchModal.filter.builder.tsx +24 -393
- package/src/components/viewer/SearchModal.filter.editors.tsx +503 -0
- package/src/components/viewer/SearchModal.filter.tsx +64 -1
- package/src/components/viewer/SearchModal.tsx +19 -6
- package/src/components/viewer/ViewerLayout.tsx +5 -0
- package/src/components/viewer/Viewport.tsx +64 -9
- package/src/components/viewer/ViewportContainer.tsx +45 -3
- package/src/components/viewer/bcf/BCFOverlay.tsx +5 -4
- package/src/components/viewer/lists/ColumnHeaderMenu.tsx +84 -0
- package/src/components/viewer/lists/ListBuilder.tsx +789 -280
- package/src/components/viewer/lists/ListGroupingBar.tsx +72 -0
- package/src/components/viewer/lists/ListPanel.tsx +49 -5
- package/src/components/viewer/lists/ListResultsTable.tsx +270 -176
- package/src/components/viewer/lists/list-table-utils.ts +123 -0
- package/src/components/viewer/useGeometryStreaming.ts +21 -1
- package/src/generated/mcp-catalog.json +4 -0
- package/src/hooks/ingest/streamCleanup.test.ts +41 -0
- package/src/hooks/ingest/streamCleanup.ts +45 -0
- package/src/hooks/ingest/viewerModelIngest.ts +64 -42
- package/src/hooks/ingest/watchedGeometryStream.test.ts +78 -0
- package/src/hooks/ingest/watchedGeometryStream.ts +76 -0
- package/src/hooks/source-key.ts +35 -0
- package/src/hooks/useAlignmentLines3D.ts +139 -0
- package/src/hooks/useClash.ts +420 -0
- package/src/hooks/useGridLines3D.ts +140 -0
- package/src/hooks/useIfcFederation.ts +16 -2
- package/src/hooks/useIfcLoader.ts +5 -7
- package/src/lib/clash/persistence.ts +308 -0
- package/src/lib/geo/effective-georef.test.ts +66 -0
- package/src/lib/length-unit-scale.ts +41 -0
- package/src/lib/lists/adapter.ts +136 -11
- package/src/lib/lists/export/csv.ts +47 -0
- package/src/lib/lists/export/index.ts +49 -0
- package/src/lib/lists/export/model.ts +111 -0
- package/src/lib/lists/export/pdf.ts +67 -0
- package/src/lib/lists/export/xlsx.ts +83 -0
- package/src/lib/lists/index.ts +2 -0
- package/src/lib/search/filter-evaluate.test.ts +81 -0
- package/src/lib/search/filter-evaluate.ts +59 -87
- package/src/lib/search/filter-match.ts +167 -0
- package/src/lib/search/filter-rules.test.ts +25 -0
- package/src/lib/search/filter-rules.ts +75 -2
- package/src/lib/search/filter-schema.ts +0 -0
- package/src/lib/slab-edit.test.ts +72 -0
- package/src/lib/slab-edit.ts +159 -19
- package/src/sdk/adapters/export-adapter.ts +3 -3
- package/src/sdk/adapters/query-adapter.ts +3 -3
- package/src/services/extensions/host.ts +13 -0
- package/src/store/constants.ts +33 -25
- package/src/store/index.ts +29 -8
- package/src/store/slices/clashSlice.ts +251 -0
- package/src/store/slices/listSlice.ts +6 -0
- package/src/store/slices/mutationSlice.ts +14 -6
- package/src/store/slices/searchSlice.ts +29 -3
- package/src/store/slices/visibilitySlice.test.ts +23 -5
- package/src/store/slices/visibilitySlice.ts +18 -8
- package/src/utils/nativeSpatialDataStore.ts +6 -0
- package/src/utils/serverDataModel.test.ts +6 -0
- package/src/utils/serverDataModel.ts +7 -0
- package/dist/assets/geometry.worker-Cyn5BybV.js +0 -1
- package/dist/assets/ifc-lite_bg-ksLBP5cA.wasm +0 -0
- package/dist/assets/index-Bws3UAkj.css +0 -1
- package/dist/assets/raw-R2QfzPAR.js +0 -1
- package/dist/assets/server-client-Ctk8_Bof.js +0 -626
|
@@ -45,6 +45,8 @@ import {
|
|
|
45
45
|
useSymbolicAnnotationsRichData,
|
|
46
46
|
type SectionClipForGrid,
|
|
47
47
|
} from '../../hooks/useSymbolicAnnotations.js';
|
|
48
|
+
import { useAlignmentLines3D } from '../../hooks/useAlignmentLines3D.js';
|
|
49
|
+
import { useGridLines3D } from '../../hooks/useGridLines3D.js';
|
|
48
50
|
|
|
49
51
|
interface ViewportProps {
|
|
50
52
|
geometry: MeshData[] | null;
|
|
@@ -652,19 +654,43 @@ export function Viewport({
|
|
|
652
654
|
calculateScale();
|
|
653
655
|
},
|
|
654
656
|
frameSelection: () => {
|
|
655
|
-
// Frame selection
|
|
656
|
-
|
|
657
|
+
// Frame the current selection. Prefer the full multi-selection set
|
|
658
|
+
// (Ctrl-click, box-select, a clash pair) so the camera encloses EVERY
|
|
659
|
+
// selected element; fall back to the single primary id. The set is
|
|
660
|
+
// kept in sync with selection (cleared on a plain click), so the
|
|
661
|
+
// union is always an accurate frame of what's highlighted.
|
|
657
662
|
const geom = geometryRef.current;
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
+
const set = selectedEntityIdsRef.current;
|
|
664
|
+
const single = selectedEntityIdRef.current;
|
|
665
|
+
const ids = set && set.size > 0
|
|
666
|
+
? Array.from(set)
|
|
667
|
+
: single !== null ? [single] : [];
|
|
668
|
+
if (!geom || ids.length === 0) {
|
|
669
|
+
console.warn('[Viewport] frameSelection: No selection or geometry');
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
let min: { x: number; y: number; z: number } | null = null;
|
|
673
|
+
let max: { x: number; y: number; z: number } | null = null;
|
|
674
|
+
for (const id of ids) {
|
|
675
|
+
const b = getEntityBounds(geom, id);
|
|
676
|
+
if (!b) continue;
|
|
677
|
+
if (!min || !max) {
|
|
678
|
+
min = { x: b.min.x, y: b.min.y, z: b.min.z };
|
|
679
|
+
max = { x: b.max.x, y: b.max.y, z: b.max.z };
|
|
663
680
|
} else {
|
|
664
|
-
|
|
681
|
+
min.x = Math.min(min.x, b.min.x);
|
|
682
|
+
min.y = Math.min(min.y, b.min.y);
|
|
683
|
+
min.z = Math.min(min.z, b.min.z);
|
|
684
|
+
max.x = Math.max(max.x, b.max.x);
|
|
685
|
+
max.y = Math.max(max.y, b.max.y);
|
|
686
|
+
max.z = Math.max(max.z, b.max.z);
|
|
665
687
|
}
|
|
688
|
+
}
|
|
689
|
+
if (min && max) {
|
|
690
|
+
camera.frameBounds(min, max, 300);
|
|
691
|
+
calculateScale();
|
|
666
692
|
} else {
|
|
667
|
-
console.warn('[Viewport] frameSelection:
|
|
693
|
+
console.warn('[Viewport] frameSelection: Could not get bounds for selected element');
|
|
668
694
|
}
|
|
669
695
|
},
|
|
670
696
|
orbit: (deltaX: number, deltaY: number) => {
|
|
@@ -862,6 +888,34 @@ export function Viewport({
|
|
|
862
888
|
}
|
|
863
889
|
}, [annotationVertices3D, isInitialized]);
|
|
864
890
|
|
|
891
|
+
// IfcAlignment centerlines render as thin lines (not a ribbon mesh), always
|
|
892
|
+
// on — see useAlignmentLines3D. Upload/clear mirrors the annotation overlay;
|
|
893
|
+
// a separate renderer buffer keeps alignment visibility independent.
|
|
894
|
+
const alignmentVertices3D = useAlignmentLines3D();
|
|
895
|
+
useEffect(() => {
|
|
896
|
+
const renderer = rendererRef.current;
|
|
897
|
+
if (!renderer || !isInitialized) return;
|
|
898
|
+
if (alignmentVertices3D.length === 0) {
|
|
899
|
+
renderer.clearAlignmentLines3D();
|
|
900
|
+
} else {
|
|
901
|
+
renderer.uploadAlignmentLines3D(alignmentVertices3D);
|
|
902
|
+
}
|
|
903
|
+
}, [alignmentVertices3D, isInitialized]);
|
|
904
|
+
|
|
905
|
+
// Structural-grid (IfcGridAxis) lines, gated by the `ifcGrid` type-visibility
|
|
906
|
+
// toggle (issue #967). Parsed once per source + cached; only the upload/clear
|
|
907
|
+
// is toggled so flipping visibility doesn't re-parse.
|
|
908
|
+
const gridVertices3D = useGridLines3D();
|
|
909
|
+
useEffect(() => {
|
|
910
|
+
const renderer = rendererRef.current;
|
|
911
|
+
if (!renderer || !isInitialized) return;
|
|
912
|
+
if (!ifcGridVisible || gridVertices3D.length === 0) {
|
|
913
|
+
renderer.clearGridLines3D();
|
|
914
|
+
} else {
|
|
915
|
+
renderer.uploadGridLines3D(gridVertices3D);
|
|
916
|
+
}
|
|
917
|
+
}, [gridVertices3D, ifcGridVisible, isInitialized]);
|
|
918
|
+
|
|
865
919
|
// Upload IfcAnnotation text + fill data for the WebGPU symbolic overlay
|
|
866
920
|
// pipelines. Map the hook's per-annotation records into the SymbolicFillInput
|
|
867
921
|
// / SymbolicTextInput shape the renderer expects. Empty arrays clear cleanly.
|
|
@@ -1056,6 +1110,7 @@ export function Viewport({
|
|
|
1056
1110
|
geometryContentVersion,
|
|
1057
1111
|
coordinateInfo,
|
|
1058
1112
|
isStreaming,
|
|
1113
|
+
modelCount: modelIdToIndex?.size ?? 0,
|
|
1059
1114
|
geometryBoundsRef,
|
|
1060
1115
|
pendingColorUpdates,
|
|
1061
1116
|
pendingMeshColorUpdates,
|
|
@@ -39,6 +39,26 @@ const DEFAULT_COORDINATE_INFO: CoordinateInfo = {
|
|
|
39
39
|
hasLargeCoordinates: false,
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
type Vec3Bounds = { min: { x: number; y: number; z: number }; max: { x: number; y: number; z: number } };
|
|
43
|
+
|
|
44
|
+
/** True for a real (non-placeholder, non-degenerate) bounds box. */
|
|
45
|
+
function isUsableBounds(b: Vec3Bounds | undefined): b is Vec3Bounds {
|
|
46
|
+
if (!b) return false;
|
|
47
|
+
return (
|
|
48
|
+
b.max.x > b.min.x || b.max.y > b.min.y || b.max.z > b.min.z
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Axis-aligned union of two bounds boxes (either may be undefined). */
|
|
53
|
+
function unionBounds(acc: Vec3Bounds | undefined, b: Vec3Bounds | undefined): Vec3Bounds | undefined {
|
|
54
|
+
if (!isUsableBounds(b)) return acc;
|
|
55
|
+
if (!acc) return { min: { ...b.min }, max: { ...b.max } };
|
|
56
|
+
return {
|
|
57
|
+
min: { x: Math.min(acc.min.x, b.min.x), y: Math.min(acc.min.y, b.min.y), z: Math.min(acc.min.z, b.min.z) },
|
|
58
|
+
max: { x: Math.max(acc.max.x, b.max.x), y: Math.max(acc.max.y, b.max.y), z: Math.max(acc.max.z, b.max.z) },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
export function ViewportContainer() {
|
|
43
63
|
// Drive Stacked / Solo / Exploded level display from the slice.
|
|
44
64
|
// Mount-once hook — it self-gates on mode + gap + model changes.
|
|
@@ -121,7 +141,16 @@ export function ViewportContainer() {
|
|
|
121
141
|
if (storeModels.size > 1) {
|
|
122
142
|
let totalVertices = 0;
|
|
123
143
|
let totalTriangles = 0;
|
|
124
|
-
|
|
144
|
+
// The merged coordinateInfo must cover ALL visible models, not just the
|
|
145
|
+
// first one — the renderer fits the camera to `shiftedBounds`, so a
|
|
146
|
+
// first-wins box left every model after the first off-screen (it only
|
|
147
|
+
// showed its 2D grid overlay). Union the bounds across visible models;
|
|
148
|
+
// keep the first model's frame metadata (originShift / RTC) since
|
|
149
|
+
// federated models share a coordinate frame.
|
|
150
|
+
let baseCoordInfo: CoordinateInfo | undefined;
|
|
151
|
+
let unionedShifted: Vec3Bounds | undefined;
|
|
152
|
+
let unionedOriginal: Vec3Bounds | undefined;
|
|
153
|
+
let anyLargeCoords = false;
|
|
125
154
|
let shouldRebuild = false;
|
|
126
155
|
|
|
127
156
|
if (mergedLengthsRef.current.size !== storeModels.size) {
|
|
@@ -142,8 +171,12 @@ export function ViewportContainer() {
|
|
|
142
171
|
const meshCount = model.visible ? (modelGeometry?.meshes.length ?? 0) : 0;
|
|
143
172
|
totalVertices += model.visible ? (modelGeometry?.totalVertices ?? 0) : 0;
|
|
144
173
|
totalTriangles += model.visible ? (modelGeometry?.totalTriangles ?? 0) : 0;
|
|
145
|
-
if (
|
|
146
|
-
|
|
174
|
+
if (model.visible && modelGeometry?.coordinateInfo) {
|
|
175
|
+
const ci = modelGeometry.coordinateInfo;
|
|
176
|
+
if (!baseCoordInfo) baseCoordInfo = ci;
|
|
177
|
+
anyLargeCoords = anyLargeCoords || !!ci.hasLargeCoordinates;
|
|
178
|
+
unionedShifted = unionBounds(unionedShifted, ci.shiftedBounds);
|
|
179
|
+
unionedOriginal = unionBounds(unionedOriginal, ci.originalBounds);
|
|
147
180
|
}
|
|
148
181
|
|
|
149
182
|
if (
|
|
@@ -187,6 +220,15 @@ export function ViewportContainer() {
|
|
|
187
220
|
}
|
|
188
221
|
}
|
|
189
222
|
|
|
223
|
+
const mergedCoordinateInfo: CoordinateInfo | undefined = baseCoordInfo
|
|
224
|
+
? {
|
|
225
|
+
...baseCoordInfo,
|
|
226
|
+
originalBounds: unionedOriginal ?? baseCoordInfo.originalBounds,
|
|
227
|
+
shiftedBounds: unionedShifted ?? baseCoordInfo.shiftedBounds,
|
|
228
|
+
hasLargeCoordinates: anyLargeCoords,
|
|
229
|
+
}
|
|
230
|
+
: undefined;
|
|
231
|
+
|
|
190
232
|
return {
|
|
191
233
|
meshes: mergedCacheRef.current,
|
|
192
234
|
totalVertices,
|
|
@@ -128,7 +128,7 @@ export function BCFOverlay() {
|
|
|
128
128
|
const bcfProject = useViewerStore((s) => s.bcfProject);
|
|
129
129
|
const activeTopicId = useViewerStore((s) => s.activeTopicId);
|
|
130
130
|
const setActiveTopic = useViewerStore((s) => s.setActiveTopic);
|
|
131
|
-
const
|
|
131
|
+
const openWorkspacePanel = useViewerStore((s) => s.openWorkspacePanel);
|
|
132
132
|
const models = useViewerStore((s) => s.models);
|
|
133
133
|
const loading = useViewerStore((s) => s.loading);
|
|
134
134
|
const ifcDataStore = useViewerStore((s) => s.ifcDataStore);
|
|
@@ -239,10 +239,11 @@ export function BCFOverlay() {
|
|
|
239
239
|
if (!overlay) return;
|
|
240
240
|
return overlay.onMarkerClick((topicGuid) => {
|
|
241
241
|
setActiveTopic(topicGuid);
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
// Open BCF exclusively so clicking a marker brings it to the front over any
|
|
243
|
+
// other right panel (e.g. clash), instead of leaving it behind.
|
|
244
|
+
openWorkspacePanel('bcf');
|
|
244
245
|
});
|
|
245
|
-
}, [overlayReady, setActiveTopic,
|
|
246
|
+
}, [overlayReady, setActiveTopic, openWorkspacePanel]);
|
|
246
247
|
|
|
247
248
|
return (
|
|
248
249
|
<div
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-column actions menu for the Lists results table header. Brings
|
|
7
|
+
* grouping / aggregation / sorting onto the table itself so the user never
|
|
8
|
+
* has to round-trip through the list settings.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ArrowUp, ArrowDown, Group, Ungroup, Sigma, Palette, MoreVertical } from 'lucide-react';
|
|
12
|
+
import {
|
|
13
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuTrigger,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuItem,
|
|
17
|
+
DropdownMenuCheckboxItem,
|
|
18
|
+
DropdownMenuSeparator,
|
|
19
|
+
} from '@/components/ui/dropdown-menu';
|
|
20
|
+
import { cn } from '@/lib/utils';
|
|
21
|
+
|
|
22
|
+
interface ColumnHeaderMenuProps {
|
|
23
|
+
isNumeric: boolean;
|
|
24
|
+
isGroupedBy: boolean;
|
|
25
|
+
isSummed: boolean;
|
|
26
|
+
active: boolean;
|
|
27
|
+
onSort: (dir: 'asc' | 'desc') => void;
|
|
28
|
+
onToggleGroup: () => void;
|
|
29
|
+
onToggleSum: () => void;
|
|
30
|
+
onColorBy: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ColumnHeaderMenu({
|
|
34
|
+
isNumeric, isGroupedBy, isSummed, active,
|
|
35
|
+
onSort, onToggleGroup, onToggleSum, onColorBy,
|
|
36
|
+
}: ColumnHeaderMenuProps) {
|
|
37
|
+
return (
|
|
38
|
+
<DropdownMenu>
|
|
39
|
+
<DropdownMenuTrigger asChild>
|
|
40
|
+
<button
|
|
41
|
+
aria-label="Column options"
|
|
42
|
+
onClick={(e) => e.stopPropagation()}
|
|
43
|
+
className={cn(
|
|
44
|
+
'shrink-0 rounded-sm p-0.5 transition-opacity hover:text-foreground',
|
|
45
|
+
active
|
|
46
|
+
? 'text-primary opacity-100'
|
|
47
|
+
: 'text-muted-foreground opacity-0 group-hover/col:opacity-100 data-[state=open]:opacity-100',
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<MoreVertical className="h-3 w-3" />
|
|
51
|
+
</button>
|
|
52
|
+
</DropdownMenuTrigger>
|
|
53
|
+
<DropdownMenuContent align="start" className="w-52">
|
|
54
|
+
<DropdownMenuItem className="gap-2 text-xs" onClick={() => onSort('asc')}>
|
|
55
|
+
<ArrowUp className="h-3.5 w-3.5" /> Sort ascending
|
|
56
|
+
</DropdownMenuItem>
|
|
57
|
+
<DropdownMenuItem className="gap-2 text-xs" onClick={() => onSort('desc')}>
|
|
58
|
+
<ArrowDown className="h-3.5 w-3.5" /> Sort descending
|
|
59
|
+
</DropdownMenuItem>
|
|
60
|
+
<DropdownMenuSeparator />
|
|
61
|
+
<DropdownMenuItem className="gap-2 text-xs" onClick={onToggleGroup}>
|
|
62
|
+
{isGroupedBy
|
|
63
|
+
? (<><Ungroup className="h-3.5 w-3.5" /> Remove grouping</>)
|
|
64
|
+
: (<><Group className="h-3.5 w-3.5" /> Group by this column</>)}
|
|
65
|
+
</DropdownMenuItem>
|
|
66
|
+
<DropdownMenuCheckboxItem
|
|
67
|
+
className="text-xs"
|
|
68
|
+
checked={isSummed}
|
|
69
|
+
disabled={!isNumeric}
|
|
70
|
+
onCheckedChange={onToggleSum}
|
|
71
|
+
>
|
|
72
|
+
<span className="flex items-center gap-2">
|
|
73
|
+
<Sigma className="h-3.5 w-3.5" />
|
|
74
|
+
{isNumeric ? 'Sum / total this column' : 'Sum (numeric only)'}
|
|
75
|
+
</span>
|
|
76
|
+
</DropdownMenuCheckboxItem>
|
|
77
|
+
<DropdownMenuSeparator />
|
|
78
|
+
<DropdownMenuItem className="gap-2 text-xs" onClick={onColorBy}>
|
|
79
|
+
<Palette className="h-3.5 w-3.5" /> Colour by this column
|
|
80
|
+
</DropdownMenuItem>
|
|
81
|
+
</DropdownMenuContent>
|
|
82
|
+
</DropdownMenu>
|
|
83
|
+
);
|
|
84
|
+
}
|