diff --git a/package-lock.json b/package-lock.json index db0aa9b88..afa16806b 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.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "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==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" }, "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.2", + "version": "1.6.3", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.0" + "@floating-ui/utils": "^0.2.3" } }, "node_modules/@floating-ui/devtools": { @@ -2945,15 +2945,15 @@ } }, "node_modules/@floating-ui/dom": { - "version": "1.6.5", + "version": "1.6.6", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" + "@floating-ui/utils": "^0.2.3" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.2", + "version": "0.2.3", "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.28", + "@fluentui/react-window-provider": "^2.2.27", "@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.28", - "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.28.tgz", - "integrity": "sha512-YdZ74HTaoDwlvLDzoBST80/17ExIl93tLJpTxnqK5jlJOAUVQ+mxLPF2HQEJq+SZr5IMXHsQ56w/KaZVRn72YA==", + "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==", "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.28", + "@fluentui/react-window-provider": "^2.2.27", "@fluentui/set-version": "^8.2.23", "tslib": "^2.1.0" }, @@ -14966,9 +14966,9 @@ "license": "MIT" }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", "funding": [ { "type": "opencollective", @@ -14984,9 +14984,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.1.0" }, "bin": { @@ -15142,9 +15142,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001645", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001645.tgz", + "integrity": "sha512-GFtY2+qt91kzyMk6j48dJcwJVq5uTkk71XxE3RtScx7XWRLsO7bU44LOFkOZYR8w9YMS0UhPSYpN/6rAMImmLw==", "funding": [ { "type": "opencollective", @@ -16063,12 +16063,12 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.38.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.0.tgz", - "integrity": "sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A==", + "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==", "dev": true, "dependencies": { - "browserslist": "^4.23.3" + "browserslist": "^4.23.0" }, "funding": { "type": "opencollective", diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 4cfdc3bc3..39c12c2bb 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -1119,7 +1119,7 @@ export default class Explorer { } } - public openUploadItemsPane(): void { + public openUploadItemsPanePane(): 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 deleted file mode 100644 index 10b7c3859..000000000 --- a/src/Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane.tsx +++ /dev/null @@ -1,148 +0,0 @@ -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 { - if (selectedColumnIdsSet.size === 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 6c63ab446..aaccfa3ac 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts @@ -1,6 +1,5 @@ // 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"; @@ -12,16 +11,11 @@ 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 ab6c8ee70..59ae9052a 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.test.tsx @@ -92,13 +92,7 @@ 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, "", ["pk"], { - paths: ["pk"], - kind: "Hash", - version: 2, - }), - ).toContain("select"); + expect(buildQuery(false, "")).toContain("select"); }); }); diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index 09f4a3487..b0b76b377 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -1,9 +1,10 @@ import { Item, ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { Button, Input, TableRowId, makeStyles, shorthands } from "@fluentui/react-components"; -import { Dismiss16Filled } from "@fluentui/react-icons"; +import { ArrowClockwise16Filled, 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, @@ -20,14 +21,11 @@ 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"; @@ -53,11 +51,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 { defaultQueryFields, extractPartitionKeyValues } from "../../../Utils/QueryUtils"; +import { extractPartitionKeyValues } from "../../../Utils/QueryUtils"; import DocumentId from "../../Tree/DocumentId"; import ObjectId from "../../Tree/ObjectId"; import TabsBase from "../TabsBase"; -import { ColumnDefinition, DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent"; +import { 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 @@ -103,6 +101,17 @@ 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 { @@ -272,7 +281,7 @@ const createUploadButton = (container: Explorer): CommandButtonComponentProps => iconAlt: label, onCommandClick: () => { const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); - selectedCollection && container.openUploadItemsPane(); + selectedCollection && container.openUploadItemsPanePane(); }, commandButtonLabel: label, ariaLabel: label, @@ -460,33 +469,17 @@ 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 || "{}"; } - // Filter out fields starting with "/" (partition keys) - return QueryUtils.buildDocumentsQuery( - filter, - partitionKeyProperties, - partitionKey, - additionalField?.filter((f) => !f.startsWith("/")) || [], - ); + return QueryUtils.buildDocumentsQuery(filter, partitionKeyProperties, partitionKey); }; /** @@ -529,12 +522,6 @@ 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 - 29; - // Export to expose to unit tests export const DocumentsTabComponent: React.FunctionComponent = ({ isPreferredApiMongoDB, @@ -553,7 +540,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(); @@ -584,7 +571,7 @@ export const DocumentsTabComponent: React.FunctionComponent(() => - readSubComponentState(SubComponentName.MainTabDivider, _collection, { + readSubComponentState(SubComponentName.MainTabDivider, _collection, { leftPaneWidthPercent: 35, }), ); @@ -598,8 +585,8 @@ export const DocumentsTabComponent: React.FunctionComponent(undefined); // User's filter history - const [lastFilterContents, setLastFilterContents] = useState(() => - readSubComponentState(SubComponentName.FilterHistory, _collection, [] as FilterHistory), + const [lastFilterContents, setLastFilterContents] = useState(() => + readSubComponentState(SubComponentName.FilterHistory, _collection, []), ); const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB); @@ -648,37 +635,10 @@ 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[], - ): ExtendedDocumentId => { - const extendedDocumentId = new DocumentId( + (rawDocument: DataModels.DocumentId, partitionKeyProperties: string[], partitionKeyValue: string[]) => + new DocumentId( { partitionKey, partitionKeyProperties, @@ -688,10 +648,7 @@ export const DocumentsTabComponent: React.FunctionComponent { onExecutionErrorChange(true); @@ -1144,13 +1093,7 @@ export const DocumentsTabComponent: React.FunctionComponent { @@ -1319,6 +1261,16 @@ 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; @@ -1350,7 +1302,9 @@ export const DocumentsTabComponent: React.FunctionComponent { - const item: DocumentsTableComponentItem = documentId.tableFields || { id: documentId.id() }; + const item: Record & { id: string } = { + id: documentId.id(), + }; if (partitionKeyPropertyHeaders && documentId.stringPartitionKeyValues) { for (let i = 0; i < partitionKeyPropertyHeaders.length; i++) { @@ -1361,44 +1315,6 @@ 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 @@ -1414,9 +1330,6 @@ export const DocumentsTabComponent: React.FunctionComponent { initDocumentEditor(documentId, content); - - // Update columns - setColumnDefinitionsFromDocument(content); }, ); @@ -1507,22 +1420,10 @@ export const DocumentsTabComponent: React.FunctionComponent resizeObserver.disconnect(); // clean up }, []); - // 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 columnHeaders = { + idHeader: isPreferredApiMongoDB ? "_id" : "id", + partitionKeyHeaders: (showPartitionKey(_collection, isPreferredApiMongoDB) && partitionKeyPropertyHeaders) || [], + }; const onSelectedRowsChange = (selectedRows: Set) => { confirmDiscardingChange(() => { @@ -1754,7 +1655,7 @@ export const DocumentsTabComponent: React.FunctionComponent(SubComponentName.FilterHistory, _collection, lastFilterContents); + saveSubComponentState(SubComponentName.FilterHistory, _collection, lastFilterContents); }; const refreshDocumentsGrid = useCallback( @@ -1853,41 +1754,6 @@ 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]); - return (

@@ -1982,40 +1848,42 @@ 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); }} >
-
-
- refreshDocumentsGrid(false)} - items={tableItems} - onItemClicked={(index) => onDocumentClicked(index, documentIds)} - onSelectedRowsChange={onSelectedRowsChange} - selectedRows={selectedRows} - size={tableContainerSizePx} - selectedColumnIds={selectedColumnIds} - columnDefinitions={columnDefinitions} - isRowSelectionDisabled={ - configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly - } - onColumnSelectionChange={onColumnSelectionChange} - defaultColumnSelection={getInitialColumnSelection()} - collection={_collection} - isColumnSelectionDisabled={isPreferredApiMongoDB} +
+
+
+
+ onDocumentClicked(index, documentIds)} + onSelectedRowsChange={onSelectedRowsChange} + selectedRows={selectedRows} + size={tableContainerSizePx} + columnHeaders={columnHeaders} + isSelectionDisabled={ + (partitionKey.systemKey && !isPreferredApiMongoDB) || + (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) + } + collection={_collection} + /> +
{tableItems.length > 0 && ( { height: 0, width: 0, }, - columnDefinitions: [ - { id: ID_HEADER, label: "ID", isPartitionKey: false }, - { id: PARTITION_KEY_HEADER, label: "Partition Key", isPartitionKey: true }, - ], - isRowSelectionDisabled: false, + columnHeaders: { + idHeader: ID_HEADER, + partitionKeyHeaders: [PARTITION_KEY_HEADER], + }, + isSelectionDisabled: false, collection: { databaseId: "db", id: ((): string => "coll") as ko.Observable, } as ViewModels.CollectionBase, - onRefreshTable: (): void => { - throw new Error("Function not implemented."); - }, - selectedColumnIds: [], }); it("should render documents and partition keys in header", () => { @@ -44,7 +40,7 @@ describe("DocumentsTableComponent", () => { it("should not render selection column when isSelectionDisabled is true", () => { const props: IDocumentsTableComponentProps = createMockProps(); - props.isRowSelectionDisabled = true; + props.isSelectionDisabled = true; const wrapper = mount(); expect(wrapper).toMatchSnapshot(); }); diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx index df68a2c0d..127c5d6d9 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx @@ -1,48 +1,30 @@ import { - Button, + createTableColumn, Menu, - MenuDivider, MenuItem, MenuList, MenuPopover, MenuTrigger, TableRowData as RowStateBase, - SortDirection, Table, TableBody, TableCell, TableCellLayout, TableColumnDefinition, - TableColumnId, TableColumnSizingOptions, TableHeader, TableHeaderCell, TableRow, TableRowId, TableSelectionCell, - tokens, useArrowNavigationGroup, useTableColumnSizing_unstable, useTableFeatures, useTableSelection, - useTableSort, } from "@fluentui/react-components"; -import { - ArrowClockwise16Regular, - ArrowResetRegular, - DeleteRegular, - EditRegular, - MoreHorizontalRegular, - TableResizeColumnRegular, - TextSortAscendingRegular, - TextSortDescendingRegular, -} from "@fluentui/react-icons"; import { NormalizedEventKey } from "Common/Constants"; -import { TableColumnSelectionPane } from "Explorer/Panes/TableColumnSelectionPane/TableColumnSelectionPane"; import { ColumnSizesMap, - ColumnSort, - deleteSubComponentState, readSubComponentState, saveSubComponentState, SubComponentName, @@ -50,37 +32,29 @@ import { import { INITIAL_SELECTED_ROW_INDEX, useDocumentsTabStyles } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabV2"; import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper"; import { LayoutConstants } from "Explorer/Theme/ThemeUtil"; -import { userContext } from "UserContext"; import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils"; -import { useSidePanel } from "hooks/useSidePanel"; import React, { useCallback, useMemo } from "react"; import { FixedSizeList as List, ListChildComponentProps } from "react-window"; import * as ViewModels from "../../../Contracts/ViewModels"; export type DocumentsTableComponentItem = { id: string; -} & Record; +} & Record; -export type ColumnDefinition = { - id: string; - label: string; - isPartitionKey: boolean; +export type ColumnHeaders = { + idHeader: string; + partitionKeyHeaders: string[]; }; export interface IDocumentsTableComponentProps { - onRefreshTable: () => void; items: DocumentsTableComponentItem[]; onItemClicked: (index: number) => void; onSelectedRowsChange: (selectedItemsIndices: Set) => void; selectedRows: Set; size: { height: number; width: number }; - selectedColumnIds: string[]; - columnDefinitions: ColumnDefinition[]; + columnHeaders: ColumnHeaders; style?: React.CSSProperties; - isRowSelectionDisabled?: boolean; + isSelectionDisabled?: boolean; collection: ViewModels.CollectionBase; - onColumnSelectionChange?: (newSelectedColumnIds: string[]) => void; - defaultColumnSelection?: string[]; - isColumnSelectionDisabled?: boolean; } interface TableRowData extends RowStateBase { @@ -93,33 +67,25 @@ interface ReactWindowRenderFnProps extends ListChildComponentProps { data: TableRowData[]; } -const COLUMNS_MENU_NAME = "columnsMenu"; - const defaultSize = { idealWidth: 200, minWidth: 50, }; export const DocumentsTableComponent: React.FC = ({ - onRefreshTable, items, onSelectedRowsChange, selectedRows, style, size, - selectedColumnIds, - columnDefinitions, - isRowSelectionDisabled: isSelectionDisabled, + columnHeaders, + isSelectionDisabled, collection, - onColumnSelectionChange, - defaultColumnSelection, - isColumnSelectionDisabled, }: IDocumentsTableComponentProps) => { - const styles = useDocumentsTabStyles(); - const [columnSizingOptions, setColumnSizingOptions] = React.useState(() => { + const columnIds = ["id"].concat(columnHeaders.partitionKeyHeaders); const columnSizesMap: ColumnSizesMap = readSubComponentState(SubComponentName.ColumnSizes, collection, {}); const columnSizesPx: TableColumnSizingOptions = {}; - selectedColumnIds.forEach((columnId) => { + columnIds.forEach((columnId) => { if ( !columnSizesMap || !columnSizesMap[columnId] || @@ -137,24 +103,7 @@ export const DocumentsTableComponent: React.FC = return columnSizesPx; }); - const [sortState, setSortState] = React.useState<{ - sortDirection: "ascending" | "descending"; - sortColumn: TableColumnId | undefined; - }>(() => { - const sort = readSubComponentState(SubComponentName.ColumnSort, collection, undefined); - - if (!sort) { - return { - sortDirection: undefined, - sortColumn: undefined, - }; - } - - return { - sortDirection: sort.direction, - sortColumn: sort.columnId, - }; - }); + const styles = useDocumentsTabStyles(); const onColumnResize = React.useCallback((_, { columnId, width }: { columnId: string; width: number }) => { setColumnSizingOptions((state) => { @@ -173,123 +122,42 @@ export const DocumentsTableComponent: React.FC = return acc; }, {} as ColumnSizesMap); - saveSubComponentState(SubComponentName.ColumnSizes, collection, persistentSizes, true); + saveSubComponentState(SubComponentName.ColumnSizes, collection, persistentSizes, true); return newSizingOptions; }); }, []); - // const restoreFocusTargetAttribute = useRestoreFocusTarget(); - - const onSortClick = (event: React.SyntheticEvent, columnId: string, direction: SortDirection) => { - setColumnSort(event, columnId, direction); - - if (columnId === undefined || direction === undefined) { - deleteSubComponentState(SubComponentName.ColumnSort, collection); - return; - } - - saveSubComponentState(SubComponentName.ColumnSort, collection, { columnId, direction }); - }; - // Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes const columns: TableColumnDefinition[] = useMemo( () => - columnDefinitions - .filter((column) => selectedColumnIds.includes(column.id)) - .map((column) => ({ - columnId: column.id, - compare: (a, b) => { - if (typeof a[column.id] === "string") { - return (a[column.id] as string).localeCompare(b[column.id] as string); - } else if (typeof a[column.id] === "number") { - return (a[column.id] as number) - (b[column.id] as number); - } else { - // Should not happen - return 0; - } - }, - renderHeaderCell: () => ( - <> - {column.label} - - - - - ), + [ + createTableColumn({ + columnId: "id", + compare: (a, b) => a.id.localeCompare(b.id), + renderHeaderCell: () => columnHeaders.idHeader, renderCell: (item) => ( - - {item[column.id]} + + {item.id} ), - })), - [columnDefinitions, onColumnSelectionChange, selectedColumnIds], + }), + ].concat( + columnHeaders.partitionKeyHeaders.map((pkHeader) => + createTableColumn({ + columnId: pkHeader, + compare: (a, b) => a[pkHeader].localeCompare(b[pkHeader]), + // Show Refresh button on last column + renderHeaderCell: () => {pkHeader}, + renderCell: (item) => ( + + {item[pkHeader]} + + ), + }), + ), + ), + [columnHeaders], ); const [selectionStartIndex, setSelectionStartIndex] = React.useState(INITIAL_SELECTED_ROW_INDEX); @@ -379,7 +247,6 @@ export const DocumentsTableComponent: React.FC = columnSizing_unstable: columnSizing, tableRef, selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, - sort: { getSortDirection, setColumnSort, sort }, } = useTableFeatures( { columns, @@ -393,36 +260,25 @@ export const DocumentsTableComponent: React.FC = // eslint-disable-next-line react/prop-types onSelectionChange: (e, data) => onSelectedRowsChange(data.selectedItems), }), - useTableSort({ - sortState, - onSortChange: (e, nextSortState) => setSortState(nextSortState), - }), ], ); - const headerSortProps = (columnId: TableColumnId) => ({ - // onClick: (e: React.MouseEvent) => toggleColumnSort(e, columnId), - sortDirection: getSortDirection(columnId), + const rows: TableRowData[] = getRows((row) => { + const selected = isRowSelected(row.rowId); + return { + ...row, + onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId), + onKeyDown: (e: React.KeyboardEvent) => { + if (e.key === " ") { + e.preventDefault(); + toggleRow(e, row.rowId); + } + }, + selected, + appearance: selected ? ("brand" as const) : ("none" as const), + }; }); - const rows: TableRowData[] = sort( - getRows((row) => { - const selected = isRowSelected(row.rowId); - return { - ...row, - onClick: (e: React.MouseEvent) => toggleRow(e, row.rowId), - onKeyDown: (e: React.KeyboardEvent) => { - if (e.key === " ") { - e.preventDefault(); - toggleRow(e, row.rowId); - } - }, - selected, - appearance: selected ? ("brand" as const) : ("none" as const), - }; - }), - ); - const toggleAllKeydown = React.useCallback( (e: React.KeyboardEvent) => { if (e.key === " ") { @@ -448,50 +304,37 @@ export const DocumentsTableComponent: React.FC = ...style, }; - const checkedValues: { [COLUMNS_MENU_NAME]: string[] } = { - [COLUMNS_MENU_NAME]: [], - }; - columnDefinitions.forEach( - (columnDefinition) => - selectedColumnIds.includes(columnDefinition.id) && checkedValues[COLUMNS_MENU_NAME].push(columnDefinition.id), - ); - - const openColumnSelectionPane = (): void => { - useSidePanel - .getState() - .openSidePanel( - "Select columns", - , - ); - }; - return ( - +
{!isSelectionDisabled && ( )} - {columns.map((column) => ( - - {column.renderHeaderCell()} - + {columns.map((column /* index */) => ( + + + + {column.renderHeaderCell()} + + + + + + Keyboard Column Resizing + + + + ))} diff --git a/src/Explorer/Tabs/DocumentsTabV2/SelectionHelper.ts b/src/Explorer/Tabs/DocumentsTabV2/SelectionHelper.ts index 25d0d3270..fe4426d4e 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/SelectionHelper.ts +++ b/src/Explorer/Tabs/DocumentsTabV2/SelectionHelper.ts @@ -1,5 +1,3 @@ -import { useEffect, useRef } from "react"; - /** * Utility class to help with selection. * This emulates File Explorer selection behavior. @@ -92,12 +90,3 @@ export const selectionHelper = ( } } }; - -// To get previous values of a state in useEffect -export const usePrevious = (value: T): T | undefined => { - const ref = useRef(); - useEffect(() => { - ref.current = value; - }); - return ref.current; -}; diff --git a/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap b/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap index 15dae99d7..93719e55c 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap +++ b/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap @@ -55,57 +55,53 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = ` } >
- } + onClick={[Function]} + onKeyDown={[Function]} + size="small" + style={ { - "databaseId": "databaseId", - "id": [Function], - } - } - columnDefinitions={ - [ - { - "id": "id", - "isPartitionKey": false, - "label": "id", - }, - ] - } - defaultColumnSelection={ - [ - "id", - ] - } - isColumnSelectionDisabled={false} - isRowSelectionDisabled={true} - items={[]} - onColumnSelectionChange={[Function]} - onItemClicked={[Function]} - onRefreshTable={[Function]} - onSelectedRowsChange={[Function]} - selectedColumnIds={ - [ - "id", - ] - } - selectedRows={ - Set { - 0, + "color": undefined, } } />
+
+ +
+ > + + + + + , + }, + } + } + > + + + } + className="___16q6g07_0000000 finvdd3 fjik90z fw35ms5" + data-tabster="{"restorer":{"type":1}}" + id="menu17" + key="id" + onContextMenu={[Function]} + onMouseEnter={[Function]} + onMouseLeave={[Function]} + onMouseMove={[Function]} + style={ + { + "maxWidth": 50, + "minWidth": 50, + "width": 50, + } + } + > + + + + + , + }, + } + } + > + + + + + + + + @@ -268,7 +509,106 @@ exports[`DocumentsTableComponent should not render selection column when isSelec "width": "100%", } } - /> + > + +
+ +
+
+ + 1 + +
+
+
+
+
+ +
+ +
+
+ + pk1 + +
+
+
+
+
+ + > + +
+ +
+
+ + 2 + +
+
+
+
+
+ +
+ +
+
+ + pk2 + +
+
+
+
+
+
+ > + +
+ +
+
+ + 3 + +
+
+
+
+
+ +
+ +
+
+ + pk3 + +
+
+
+
+
+
@@ -469,21 +1007,15 @@ exports[`DocumentsTableComponent should render documents and partition keys in h "id": [Function], } } - columnDefinitions={ - [ - { - "id": "id", - "isPartitionKey": false, - "label": "ID", - }, - { - "id": "partitionKey", - "isPartitionKey": true, - "label": "Partition Key", - }, - ] + columnHeaders={ + { + "idHeader": "id", + "partitionKeyHeaders": [ + "partitionKey", + ], + } } - isRowSelectionDisabled={false} + isSelectionDisabled={false} items={ [ { @@ -501,9 +1033,7 @@ exports[`DocumentsTableComponent should render documents and partition keys in h ] } onItemClicked={[Function]} - onRefreshTable={[Function]} onSelectedRowsChange={[Function]} - selectedColumnIds={[]} selectedRows={Set {}} size={ { @@ -518,7 +1048,6 @@ exports[`DocumentsTableComponent should render documents and partition keys in h noNativeElements={true} role="grid" size="small" - sortable={true} style={ { "minWidth": "fit-content", @@ -565,7 +1094,6 @@ exports[`DocumentsTableComponent should render documents and partition keys in h } } checked={false} - key="selectcell" onClick={[Function]} onKeyDown={[Function]} > @@ -599,6 +1127,255 @@ exports[`DocumentsTableComponent should render documents and partition keys in h + + + + + , + }, + } + } + > + + + } + className="___16q6g07_0000000 finvdd3 fjik90z fw35ms5" + data-tabster="{"restorer":{"type":1}}" + id="menu3" + key="id" + onContextMenu={[Function]} + onMouseEnter={[Function]} + onMouseLeave={[Function]} + onMouseMove={[Function]} + style={ + { + "maxWidth": 50, + "minWidth": 50, + "width": 50, + } + } + > + + + + + , + }, + } + } + > + + + + + + + @@ -800,7 +1577,7 @@ exports[`DocumentsTableComponent should render documents and partition keys in h aria-label="Select row" checked={false} className="fui-Checkbox__input ruo9svu ___qlal8r0_1xrlghj f1vgc2s3" - id="checkbox-10" + id="checkbox-12" onChange={[Function]} type="checkbox" /> @@ -812,6 +1589,104 @@ exports[`DocumentsTableComponent should render documents and partition keys in h + +
+ +
+
+ + 1 + +
+
+
+
+
+ +
+ +
+
+ + pk1 + +
+
+
+
+
@@ -931,7 +1806,7 @@ exports[`DocumentsTableComponent should render documents and partition keys in h aria-label="Select row" checked={false} className="fui-Checkbox__input ruo9svu ___qlal8r0_1xrlghj f1vgc2s3" - id="checkbox-12" + id="checkbox-14" onChange={[Function]} type="checkbox" /> @@ -943,6 +1818,104 @@ exports[`DocumentsTableComponent should render documents and partition keys in h + +
+ +
+
+ + 2 + +
+
+
+
+
+ +
+ +
+
+ + pk2 + +
+
+
+
+
@@ -1062,7 +2035,7 @@ exports[`DocumentsTableComponent should render documents and partition keys in h aria-label="Select row" checked={false} className="fui-Checkbox__input ruo9svu ___qlal8r0_1xrlghj f1vgc2s3" - id="checkbox-14" + id="checkbox-16" onChange={[Function]} type="checkbox" /> @@ -1074,6 +2047,104 @@ exports[`DocumentsTableComponent should render documents and partition keys in h + +
+ +
+
+ + 3 + +
+
+
+
+
+ +
+ +
+
+ + pk3 + +
+
+
+
+
diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 2ae14e59e..5bd84516e 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -38,7 +38,6 @@ export type Features = { readonly copilotChatFixedMonacoEditorHeight: boolean; readonly enablePriorityBasedExecution: boolean; readonly disableConnectionStringLogin: boolean; - readonly enableDocumentsTableColumnSelection: boolean; // can be set via both flight and feature flag autoscaleDefault: boolean; @@ -109,7 +108,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"), enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"), disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"), - enableDocumentsTableColumnSelection: "true" === get("enabledocumentstablecolumnselection"), }; } diff --git a/src/Shared/StorageUtility.ts b/src/Shared/StorageUtility.ts index 7a55513ed..1cea30145 100644 --- a/src/Shared/StorageUtility.ts +++ b/src/Shared/StorageUtility.ts @@ -29,7 +29,6 @@ export enum StorageKey { GalleryCalloutDismissed, VisitedAccounts, PriorityLevel, - DocumentsTabPrefs, DefaultQueryResultsView, AppState, } diff --git a/src/Utils/QueryUtils.ts b/src/Utils/QueryUtils.ts index 5706d4eac..5c621b2fb 100644 --- a/src/Utils/QueryUtils.ts +++ b/src/Utils/QueryUtils.ts @@ -2,28 +2,18 @@ import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; -export const defaultQueryFields = ["id", "_self", "_rid", "_ts"]; - export function buildDocumentsQuery( filter: string, partitionKeyProperties: string[], partitionKey: DataModels.PartitionKey, - additionalField: string[] = [], ): string { - const fieldSet = new Set(defaultQueryFields); - additionalField.forEach((prop) => fieldSet.add(prop)); - - const objectListSpec = [...fieldSet] - .filter((f) => !partitionKeyProperties.includes(f)) - .map((prop) => `c.${prop}`) - .join(","); let query = partitionKeyProperties && partitionKeyProperties.length > 0 - ? `select ${objectListSpec}, [${buildDocumentsQueryPartitionProjections( + ? `select c.id, c._self, c._rid, c._ts, [${buildDocumentsQueryPartitionProjections( "c", partitionKey, )}] as _partitionKeyValue from c` - : `select ${objectListSpec} from c`; + : `select c.id, c._self, c._rid, c._ts from c`; if (filter) { query += " " + filter;