diff --git a/package-lock.json b/package-lock.json index afa16806b..db0aa9b88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2527,13 +2527,13 @@ } }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", - "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.1", - "core-js-compat": "^3.36.1" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -2932,10 +2932,10 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.3", + "version": "1.6.2", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.3" + "@floating-ui/utils": "^0.2.0" } }, "node_modules/@floating-ui/devtools": { @@ -2945,15 +2945,15 @@ } }, "node_modules/@floating-ui/dom": { - "version": "1.6.6", + "version": "1.6.5", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.3" + "@floating-ui/utils": "^0.2.0" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.3", + "version": "0.2.2", "license": "MIT" }, "node_modules/@fluentui/date-time-utilities": { @@ -3501,7 +3501,7 @@ "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.8.10.tgz", "integrity": "sha512-Xvnn6uKMsinMg/zo79KBNCDABnl0gpmArQYNQya9FCNRzvmHUCDvuQCqv4IKslvPvuC0Ya8mR2NORm2w0JoZiw==", "dependencies": { - "@fluentui/react-window-provider": "^2.2.27", + "@fluentui/react-window-provider": "^2.2.28", "@fluentui/set-version": "^8.2.23", "@fluentui/utilities": "^8.15.13", "tslib": "^2.1.0" @@ -4426,9 +4426,9 @@ } }, "node_modules/@fluentui/react-window-provider": { - "version": "2.2.27", - "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.27.tgz", - "integrity": "sha512-Dg0G9bizjryV0Q/r0CPtCVTPa2II/EsT9E6JT3jPSALjQADDLlW4/+ZXbcEC7geZ/40+KpZDmhplvk/AJSFBKg==", + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.28.tgz", + "integrity": "sha512-YdZ74HTaoDwlvLDzoBST80/17ExIl93tLJpTxnqK5jlJOAUVQ+mxLPF2HQEJq+SZr5IMXHsQ56w/KaZVRn72YA==", "dependencies": { "@fluentui/set-version": "^8.2.23", "tslib": "^2.1.0" @@ -4512,7 +4512,7 @@ "dependencies": { "@fluentui/dom-utilities": "^2.3.7", "@fluentui/merge-styles": "^8.6.12", - "@fluentui/react-window-provider": "^2.2.27", + "@fluentui/react-window-provider": "^2.2.28", "@fluentui/set-version": "^8.2.23", "tslib": "^2.1.0" }, @@ -14966,9 +14966,9 @@ "license": "MIT" }, "node_modules/browserslist": { - "version": "4.23.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", - "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "funding": [ { "type": "opencollective", @@ -14984,9 +14984,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001640", - "electron-to-chromium": "^1.4.820", - "node-releases": "^2.0.14", + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, "bin": { @@ -15142,9 +15142,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001645", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001645.tgz", - "integrity": "sha512-GFtY2+qt91kzyMk6j48dJcwJVq5uTkk71XxE3RtScx7XWRLsO7bU44LOFkOZYR8w9YMS0UhPSYpN/6rAMImmLw==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "funding": [ { "type": "opencollective", @@ -16063,12 +16063,12 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", - "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "version": "3.38.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.0.tgz", + "integrity": "sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A==", "dev": true, "dependencies": { - "browserslist": "^4.23.0" + "browserslist": "^4.23.3" }, "funding": { "type": "opencollective", diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 39c12c2bb..4cfdc3bc3 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1119,7 +1119,7 @@ export default class Explorer { } } - public openUploadItemsPanePane(): void { + public openUploadItemsPane(): void { useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); } public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void { diff --git a/src/Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane.tsx b/src/Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane.tsx new file mode 100644 index 000000000..a3cb08a4a --- /dev/null +++ b/src/Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane.tsx @@ -0,0 +1,156 @@ +import { + Button, + Checkbox, + CheckboxOnChangeData, + InputOnChangeData, + makeStyles, + SearchBox, + SearchBoxChangeEvent, + Text, +} from "@fluentui/react-components"; +import { configContext } from "ConfigContext"; +import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent"; +import { CosmosFluentProvider, getPlatformTheme } from "Explorer/Theme/ThemeUtil"; +import React from "react"; +import { useSidePanel } from "../../../hooks/useSidePanel"; + +const useColumnSelectionStyles = makeStyles({ + paneContainer: { + height: "100%", + display: "flex", + }, + searchBox: { + width: "100%", + }, + checkboxContainer: { + display: "flex", + flexDirection: "column", + flex: 1, + }, + checkboxLabel: { + padding: "4px 8px", + marginBottom: "0px", + }, +}); +export interface TableColumnSelectionPaneProps { + columnDefinitions: ColumnDefinition[]; + selectedColumnIds: string[]; + onSelectionChange: (newSelectedColumnIds: string[]) => void; + defaultSelection: string[]; +} + +export const TableColumnSelectionPane: React.FC = ({ + columnDefinitions, + selectedColumnIds, + onSelectionChange, + defaultSelection, +}: TableColumnSelectionPaneProps): JSX.Element => { + const closeSidePanel = useSidePanel((state) => state.closeSidePanel); + const originalSelectedColumnIds = React.useMemo(() => selectedColumnIds, []); + const [columnSearchText, setColumnSearchText] = React.useState(""); + const [newSelectedColumnIds, setNewSelectedColumnIds] = React.useState(originalSelectedColumnIds); + const styles = useColumnSelectionStyles(); + + const selectedColumnIdsSet = new Set(newSelectedColumnIds); + const onCheckedValueChange = (id: string, checkedData?: CheckboxOnChangeData): void => { + const checked = checkedData?.checked; + if (checked === "mixed" || checked === undefined) { + return; + } + + if (checked) { + selectedColumnIdsSet.add(id); + } else { + /* selectedColumnIds may contain ids that are not in columnDefinitions, because the selected + * ids may have been loaded from persistence, but don't exist in the current retrieved documents. + */ + + if ( + Array.from(selectedColumnIdsSet).filter((id) => columnDefinitions.find((def) => def.id === id) !== undefined) + .length === 1 && + selectedColumnIdsSet.has(id) + ) { + // Don't allow unchecking the last column + return; + } + selectedColumnIdsSet.delete(id); + } + setNewSelectedColumnIds([...selectedColumnIdsSet]); + }; + + const onSave = (): void => { + onSelectionChange(newSelectedColumnIds); + closeSidePanel(); + }; + + const onSearchChange: (event: SearchBoxChangeEvent, data: InputOnChangeData) => void = (_, data) => + // eslint-disable-next-line react/prop-types + setColumnSearchText(data.value); + + const theme = getPlatformTheme(configContext.platform); + + // Filter and move partition keys to the top + const columnDefinitionList = columnDefinitions + .filter((def) => !columnSearchText || def.label.toLowerCase().includes(columnSearchText.toLowerCase())) + .sort((a, b) => { + const ID = "id"; + // "id" always at the top, then partition keys, then everything else sorted + if (a.id === ID) { + return b.id === ID ? 0 : -1; + } else if (b.id === ID) { + return a.id === ID ? 0 : 1; + } else if (a.isPartitionKey && !b.isPartitionKey) { + return -1; + } else if (b.isPartitionKey && !a.isPartitionKey) { + return 1; + } else { + return a.label.localeCompare(b.label); + } + }); + + return ( +
+ +
+
+ Select which columns to display in your view of items in your container. +
to avoid margin-bottom set by panelMainContent css */> + +
+ +
+ {columnDefinitionList.map((columnDefinition) => ( + onCheckedValueChange(columnDefinition.id, data)} + /> + ))} +
+ +
+
+ + +
+
+
+
+ ); +}; diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts index aaccfa3ac..6c63ab446 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts @@ -1,5 +1,6 @@ // Definitions of State data +import { ColumnDefinition } from "Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent"; import { deleteState, loadState, saveState, saveStateDebounced } from "Shared/AppStatePersistenceUtility"; import { userContext } from "UserContext"; import * as ViewModels from "../../../Contracts/ViewModels"; @@ -11,11 +12,16 @@ export enum SubComponentName { ColumnSizes = "ColumnSizes", FilterHistory = "FilterHistory", MainTabDivider = "MainTabDivider", + ColumnsSelection = "ColumnsSelection", + ColumnSort = "ColumnSort", } export type ColumnSizesMap = { [columnId: string]: WidthDefinition }; +export type FilterHistory = string[]; export type WidthDefinition = { widthPx: number }; export type TabDivider = { leftPaneWidthPercent: number }; +export type ColumnsSelection = { selectedColumnIds: string[]; columnDefinitions: ColumnDefinition[] }; +export type ColumnSort = { columnId: string; direction: "ascending" | "descending" }; /** * diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx index 59ae9052a..ab6c8ee70 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx @@ -92,7 +92,13 @@ async function waitForComponentToPaint

(wrapper: ReactWrapper

| S describe("Documents tab (noSql API)", () => { describe("buildQuery", () => { it("should generate the right select query for SQL API", () => { - expect(buildQuery(false, "")).toContain("select"); + expect( + buildQuery(false, "", ["pk"], { + paths: ["pk"], + kind: "Hash", + version: 2, + }), + ).toContain("select"); }); }); diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index e6b726bca..67358b739 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -1,10 +1,9 @@ import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { Button, Input, TableRowId, makeStyles, shorthands } from "@fluentui/react-components"; -import { ArrowClockwise16Filled, Dismiss16Filled } from "@fluentui/react-icons"; +import { Dismiss16Filled } from "@fluentui/react-icons"; import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants"; import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import MongoUtility from "Common/MongoUtility"; -import { StyleConstants } from "Common/StyleConstants"; import { createDocument } from "Common/dataAccess/createDocument"; import { deleteDocument as deleteNoSqlDocument, @@ -21,11 +20,14 @@ import Explorer from "Explorer/Explorer"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { + ColumnsSelection, + FilterHistory, SubComponentName, TabDivider, readSubComponentState, saveSubComponentState, } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil"; +import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper"; import { CosmosFluentProvider, LayoutConstants, cosmosShorthands, tokens } from "Explorer/Theme/ThemeUtil"; import { useSelectedNode } from "Explorer/useSelectedNode"; import { KeyboardAction, KeyboardActionGroup, useKeyboardActionGroup } from "KeyboardShortcuts"; @@ -51,11 +53,11 @@ import * as ViewModels from "../../../Contracts/ViewModels"; import { CollectionBase } from "../../../Contracts/ViewModels"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as QueryUtils from "../../../Utils/QueryUtils"; -import { extractPartitionKeyValues } from "../../../Utils/QueryUtils"; +import { defaultQueryFields, extractPartitionKeyValues } from "../../../Utils/QueryUtils"; import DocumentId from "../../Tree/DocumentId"; import ObjectId from "../../Tree/ObjectId"; import TabsBase from "../TabsBase"; -import { DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent"; +import { ColumnDefinition, DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent"; const MAX_FILTER_HISTORY_COUNT = 100; // Datalist will become scrollable, so we can afford to keep more items than fit on the screen @@ -89,6 +91,13 @@ export const useDocumentsTabStyles = makeStyles({ tableCell: { ...cosmosShorthands.borderLeft(), }, + tableHeader: { + display: "flex", + }, + tableHeaderFiller: { + width: "20px", + boxShadow: `0px -1px ${tokens.colorNeutralStroke2} inset`, + }, loadMore: { ...cosmosShorthands.borderTop(), display: "grid", @@ -101,17 +110,6 @@ export const useDocumentsTabStyles = makeStyles({ ...shorthands.outline("1px", "dotted"), }, }, - floatingControlsContainer: { - position: "relative", - }, - floatingControls: { - position: "absolute", - top: "6px", - right: 0, - float: "right", - backgroundColor: "white", - zIndex: 1, - }, }); export class DocumentsTabV2 extends TabsBase { @@ -281,7 +279,7 @@ const createUploadButton = (container: Explorer): CommandButtonComponentProps => iconAlt: label, onCommandClick: () => { const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); - selectedCollection && container.openUploadItemsPanePane(); + selectedCollection && container.openUploadItemsPane(); }, commandButtonLabel: label, ariaLabel: label, @@ -469,17 +467,33 @@ export const showPartitionKey = (collection: ViewModels.CollectionBase, isPrefer }; // Export to expose to unit tests +/** + * Build default query + * @param isMongo true if mongo api + * @param filter + * @param partitionKeyProperties optional for mongo + * @param partitionKey optional for mongo + * @param additionalField + * @returns + */ export const buildQuery = ( isMongo: boolean, filter: string, partitionKeyProperties?: string[], partitionKey?: DataModels.PartitionKey, + additionalField?: string[], ): string => { if (isMongo) { return filter || "{}"; } - return QueryUtils.buildDocumentsQuery(filter, partitionKeyProperties, partitionKey); + // Filter out fields starting with "/" (partition keys) + return QueryUtils.buildDocumentsQuery( + filter, + partitionKeyProperties, + partitionKey, + additionalField?.filter((f) => !f.startsWith("/")) || [], + ); }; /** @@ -522,6 +536,12 @@ const getDefaultSqlFilters = (partitionKeys: string[]) => ); const defaultMongoFilters = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"]; +// Extend DocumentId to include fields displayed in the table +type ExtendedDocumentId = DocumentId & { tableFields?: DocumentsTableComponentItem }; + +// This is based on some heuristics +const calculateOffset = (columnNumber: number): number => columnNumber * 16 - 27; + // Export to expose to unit tests export const DocumentsTabComponent: React.FunctionComponent = ({ isPreferredApiMongoDB, @@ -540,7 +560,7 @@ export const DocumentsTabComponent: React.FunctionComponent(false); const [appliedFilter, setAppliedFilter] = useState(""); const [filterContent, setFilterContent] = useState(""); - const [documentIds, setDocumentIds] = useState([]); + const [documentIds, setDocumentIds] = useState([]); const [isExecuting, setIsExecuting] = useState(false); const filterInput = useRef(null); const styles = useDocumentsTabStyles(); @@ -571,7 +591,7 @@ export const DocumentsTabComponent: React.FunctionComponent(() => - readSubComponentState(SubComponentName.MainTabDivider, _collection, { + readSubComponentState(SubComponentName.MainTabDivider, _collection, { leftPaneWidthPercent: 35, }), ); @@ -585,8 +605,8 @@ export const DocumentsTabComponent: React.FunctionComponent(undefined); // User's filter history - const [lastFilterContents, setLastFilterContents] = useState(() => - readSubComponentState(SubComponentName.FilterHistory, _collection, []), + const [lastFilterContents, setLastFilterContents] = useState(() => + readSubComponentState(SubComponentName.FilterHistory, _collection, [] as FilterHistory), ); const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB); @@ -635,10 +655,37 @@ export const DocumentsTabComponent: React.FunctionComponent { + const defaultColumnsIds = ["id"]; + if (showPartitionKey(_collection, isPreferredApiMongoDB)) { + defaultColumnsIds.push(...partitionKeyPropertyHeaders); + } + + return defaultColumnsIds; + }; + + const [selectedColumnIds, setSelectedColumnIds] = useState(() => { + const persistedColumnsSelection = readSubComponentState( + SubComponentName.ColumnsSelection, + _collection, + undefined, + ); + + if (!persistedColumnsSelection) { + return getInitialColumnSelection(); + } + + return persistedColumnsSelection.selectedColumnIds; + }); + // new DocumentId() requires a DocumentTab which we mock with only the required properties const newDocumentId = useCallback( - (rawDocument: DataModels.DocumentId, partitionKeyProperties: string[], partitionKeyValue: string[]) => - new DocumentId( + ( + rawDocument: DataModels.DocumentId, + partitionKeyProperties: string[], + partitionKeyValue: string[], + ): ExtendedDocumentId => { + const extendedDocumentId = new DocumentId( { partitionKey, partitionKeyProperties, @@ -648,7 +695,10 @@ export const DocumentsTabComponent: React.FunctionComponent { onExecutionErrorChange(true); @@ -1103,7 +1161,13 @@ export const DocumentsTabComponent: React.FunctionComponent { @@ -1271,16 +1336,6 @@ export const DocumentsTabComponent: React.FunctionComponent = (event) => { - if (event.key === " " || event.key === "Enter") { - const focusElement = event.target as HTMLElement; - refreshDocumentsGrid(false); - focusElement && focusElement.focus(); - event.stopPropagation(); - event.preventDefault(); - } - }; - const onLoadMoreKeyInput: KeyboardEventHandler = (event) => { if (event.key === " " || event.key === "Enter") { const focusElement = event.target as HTMLElement; @@ -1312,9 +1367,7 @@ export const DocumentsTabComponent: React.FunctionComponent { - const item: Record & { id: string } = { - id: documentId.id(), - }; + const item: DocumentsTableComponentItem = documentId.tableFields || { id: documentId.id() }; if (partitionKeyPropertyHeaders && documentId.stringPartitionKeyValues) { for (let i = 0; i < partitionKeyPropertyHeaders.length; i++) { @@ -1325,6 +1378,44 @@ export const DocumentsTabComponent: React.FunctionComponent { + let columnDefinitions: ColumnDefinition[] = Object.keys(document) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .filter((key) => typeof (document as any)[key] === "string" || typeof (document as any)[key] === "number") // Only allow safe types for displayable React children + .map((key) => + key === "id" + ? { id: key, label: isPreferredApiMongoDB ? "_id" : "id", isPartitionKey: false } + : { id: key, label: key, isPartitionKey: false }, + ); + + if (showPartitionKey(_collection, isPreferredApiMongoDB)) { + columnDefinitions.push( + ...partitionKeyPropertyHeaders.map((key) => ({ id: key, label: key, isPartitionKey: true })), + ); + + // Remove properties that are the partition keys, since they are already included + columnDefinitions = columnDefinitions.filter( + (columnDefinition) => !partitionKeyProperties.includes(columnDefinition.id), + ); + } + + return columnDefinitions; + }; + + /** + * Extract column definitions from document and add to the definitions + * @param document + */ + const setColumnDefinitionsFromDocument = (document: unknown): void => { + const currentIds = new Set(columnDefinitions.map((columnDefinition) => columnDefinition.id)); + extractColumnDefinitionsFromDocument(document).forEach((columnDefinition) => { + if (!currentIds.has(columnDefinition.id)) { + columnDefinitions.push(columnDefinition); + } + }); + setColumnDefinitions([...columnDefinitions]); + }; + /** * replicate logic of selectedDocument.click(); * Document has been clicked on in table @@ -1340,6 +1431,9 @@ export const DocumentsTabComponent: React.FunctionComponent { initDocumentEditor(documentId, content); + + // Update columns + setColumnDefinitionsFromDocument(content); }, ); @@ -1430,10 +1524,22 @@ export const DocumentsTabComponent: React.FunctionComponent resizeObserver.disconnect(); // clean up }, []); - const columnHeaders = { - idHeader: isPreferredApiMongoDB ? "_id" : "id", - partitionKeyHeaders: (showPartitionKey(_collection, isPreferredApiMongoDB) && partitionKeyPropertyHeaders) || [], - }; + // Column definition is a map to garantee uniqueness + const [columnDefinitions, setColumnDefinitions] = useState(() => { + const persistedColumnsSelection = readSubComponentState( + SubComponentName.ColumnsSelection, + _collection, + undefined, + ); + + if (!persistedColumnsSelection) { + return extractColumnDefinitionsFromDocument({ + id: "id", + }); + } + + return persistedColumnsSelection.columnDefinitions; + }); const onSelectedRowsChange = (selectedRows: Set) => { confirmDiscardingChange(() => { @@ -1665,7 +1771,7 @@ export const DocumentsTabComponent: React.FunctionComponent(SubComponentName.FilterHistory, _collection, lastFilterContents); }; const refreshDocumentsGrid = useCallback( @@ -1764,6 +1870,41 @@ export const DocumentsTabComponent: React.FunctionComponent { + // Do not allow to unselecting all columns + if (newSelectedColumnIds.length === 0) { + return; + } + + setSelectedColumnIds(newSelectedColumnIds); + + saveSubComponentState(SubComponentName.ColumnsSelection, _collection, { + selectedColumnIds: newSelectedColumnIds, + columnDefinitions, + }); + }; + + const prevSelectedColumnIds = usePrevious({ selectedColumnIds, setSelectedColumnIds }); + + useEffect(() => { + // If we are adding a field, let's refresh to include the field in the query + let addedField = false; + for (const field of selectedColumnIds) { + if ( + !defaultQueryFields.includes(field) && + prevSelectedColumnIds && + !prevSelectedColumnIds.selectedColumnIds.includes(field) + ) { + addedField = true; + break; + } + } + + if (addedField) { + refreshDocumentsGrid(false); + } + }, [prevSelectedColumnIds, refreshDocumentsGrid, selectedColumnIds]); + // TODO: remove isMongoBulkDeleteDisabled when new mongo proxy is enabled for all users // TODO: remove partitionKey.systemKey when JS SDK bug is fixed const isMongoBulkDeleteDisabled = !MongoProxyClient.useMongoProxyEndpoint("bulkdelete"); @@ -1865,42 +2006,41 @@ export const DocumentsTabComponent: React.FunctionComponent { tabStateData.leftPaneWidthPercent = (100 * sizes[0]) / (sizes[0] + sizes[1]); - saveSubComponentState(SubComponentName.MainTabDivider, _collection, tabStateData); + saveSubComponentState(SubComponentName.MainTabDivider, _collection, tabStateData); setTabStateData(tabStateData); }} >

-
-
-