diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts index 507d96d7c..611116bee 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil.ts @@ -1,7 +1,7 @@ // Definitions of State data import { TableColumnSizingOptions } from "@fluentui/react-components"; -import { loadState, saveStateDebounced } from "Shared/AppStatePersistenceUtility"; +import { loadState, saveState, saveStateDebounced } from "Shared/AppStatePersistenceUtility"; import { userContext } from "UserContext"; // Component states @@ -72,3 +72,36 @@ export const saveColumnSizes = (databaseName: string, containerName: string, col columnSizesMap, ); }; + +const filterHistorySubComponentName = "FilterHistory"; +export type FilterHistory = string[]; +export const readFilterHistory = (databaseName: string, containerName: string): FilterHistory => { + const globalAccountName = userContext.databaseAccount?.name; + // TODO what if databaseAccount doesn't exist? + + const state = loadState({ + globalAccountName, + databaseName, + containerName, + componentName: ComponentName, + subComponentName: filterHistorySubComponentName, + }) as FilterHistory; + + return state || []; +}; + +export const saveFilterHistory = (databaseName: string, containerName: string, filterHistory: FilterHistory): void => { + const globalAccountName = userContext.databaseAccount?.name; + // TODO what if databaseAccount doesn't exist? + + saveState( + { + componentName: ComponentName, + subComponentName: filterHistorySubComponentName, + globalAccountName, + databaseName, + containerName, + }, + filterHistory, + ); +}; diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index 9268f1b96..74bd95a6f 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -24,7 +24,9 @@ import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/ import { DocumentsTabStateData, readDocumentsTabState, + readFilterHistory, saveDocumentsTabState, + saveFilterHistory, } from "Explorer/Tabs/DocumentsTabV2/DocumentsTabStateUtil"; import { getPlatformTheme } from "Explorer/Theme/ThemeUtil"; import { useSelectedNode } from "Explorer/useSelectedNode"; @@ -425,6 +427,24 @@ export const buildQuery = ( return QueryUtils.buildDocumentsQuery(filter, partitionKeyProperties, partitionKey); }; +/** + * Export to expose to unit tests + * + * Add array2 to array1 without duplicates + * @param array1 + * @param array2 + * @return array1 with array2 added without duplicates + */ +export const addStringsNoDuplicate = (array1: string[], array2: string[]): string[] => { + const result = [...array1]; + array2.forEach((item) => { + if (!result.includes(item)) { + result.push(item); + } + }); + return result; +}; + // Export to expose to unit tests export interface IDocumentsTabComponentProps { isPreferredApiMongoDB: boolean; @@ -439,6 +459,9 @@ export interface IDocumentsTabComponentProps { isTabActive: boolean; } +const defaultSqlFilters = ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC']; +const defaultMongoFilters = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"]; + // Export to expose to unit tests export const DocumentsTabComponent: React.FunctionComponent = ({ isPreferredApiMongoDB, @@ -496,6 +519,11 @@ export const DocumentsTabComponent: React.FunctionComponent(undefined); + // User's filter history + const [lastFilterContents, setLastFilterContents] = useState(() => + readFilterHistory(_collection.databaseId, _collection.id()), + ); + const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB); useEffect(() => { @@ -521,8 +549,6 @@ export const DocumentsTabComponent: React.FunctionComponent { if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) { partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, ""); @@ -1654,6 +1679,20 @@ export const DocumentsTabComponent: React.FunctionComponent { + refreshDocumentsGrid(true); + + // Remove duplicates, but keep order + if (lastFilterContents.includes(filterContent)) { + lastFilterContents.splice(lastFilterContents.indexOf(filterContent), 1); + } + + // Save filter content to local storage + lastFilterContents.unshift(filterContent); + setLastFilterContents([...lastFilterContents]); + saveFilterHistory(_collection.databaseId, _collection.id(), lastFilterContents); + }; + const refreshDocumentsGrid = useCallback( (applyFilterButtonPressed: boolean): void => { // clear documents grid @@ -1734,7 +1773,10 @@ export const DocumentsTabComponent: React.FunctionComponent - {lastFilterContents.map((filter) => ( + {addStringsNoDuplicate( + lastFilterContents, + isPreferredApiMongoDB ? defaultMongoFilters : defaultSqlFilters, + ).map((filter) => ( @@ -1743,7 +1785,7 @@ export const DocumentsTabComponent: React.FunctionComponent refreshDocumentsGrid(true)} + onClick={onApplyFilterClick} disabled={!applyFilterButton.enabled} aria-label="Apply filter" tabIndex={0} diff --git a/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap b/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap index 52bc636a8..a8e1ceda4 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap +++ b/src/Explorer/Tabs/DocumentsTabV2/__snapshots__/DocumentsTabV2.test.tsx.snap @@ -485,6 +485,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = ` > diff --git a/src/Shared/AppStatePersistenceUtility.ts b/src/Shared/AppStatePersistenceUtility.ts index b596c7f1a..29700f9fc 100644 --- a/src/Shared/AppStatePersistenceUtility.ts +++ b/src/Shared/AppStatePersistenceUtility.ts @@ -50,40 +50,26 @@ export const saveStateDebounced = (path: StorePath, state: unknown, debounceDela timeoutId = setTimeout(() => saveState(path, state), debounceDelayMs); }; -// Internal stored state -// interface ApplicationState { -// data: GlobalStateData; -// globalAccounts: { -// [globalAccountName: string]: { -// data: GlobalAccountStateData; -// databases: { -// [databaseName: string]: { -// data: DatabaseStateData; -// containers: { -// data: ContainerStateData; -// [containerName: string]: { -// [componentName: string]: BaseStateData; -// }; -// }; -// }; -// }; -// }; -// }; -// } - interface ApplicationState { [statePath: string]: StateData; } +const orderedPathSegments: (keyof StorePath)[] = [ + "subComponentName", + "globalAccountName", + "databaseName", + "containerName", +]; + /** - * /componentName/globalAccountName/databaseName/containerName/ + * /componentName/subComponentName/globalAccountName/databaseName/containerName/ * Any of the path segments can be "" except componentName * @param path */ const createKeyFromPath = (path: StorePath): string => { - let key = `/${path.componentName}`; - ["subComponentName", "globalAccountName", "databaseName", "containerName"].forEach((segment) => { - const segmentValue = (path as any)[segment]; + let key = `/${path.componentName}`; // ComponentName is always there + orderedPathSegments.forEach((segment) => { + const segmentValue = path[segment as keyof StorePath]; key += `/${segmentValue !== undefined ? segmentValue : ""}`; }); return key;