mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-03-13 05:15:30 +00:00
Add column selection from right-click
This commit is contained in:
parent
55df5cb121
commit
b096fa9bf8
1572
package-lock.json
generated
1572
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -13,7 +13,7 @@
|
|||||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||||
"@fluentui/react": "8.112.1",
|
"@fluentui/react": "8.112.1",
|
||||||
"@fluentui/react-components": "9.34.0",
|
"@fluentui/react-components": "9.54.2",
|
||||||
"@jupyterlab/services": "6.0.2",
|
"@jupyterlab/services": "6.0.2",
|
||||||
"@jupyterlab/terminal": "3.0.3",
|
"@jupyterlab/terminal": "3.0.3",
|
||||||
"@microsoft/applicationinsights-web": "2.6.1",
|
"@microsoft/applicationinsights-web": "2.6.1",
|
||||||
@ -245,4 +245,4 @@
|
|||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"endOfLine": "auto"
|
"endOfLine": "auto"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -18,6 +18,7 @@ import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
|||||||
import Explorer from "Explorer/Explorer";
|
import Explorer from "Explorer/Explorer";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
|
import { usePrevious } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||||
import {
|
import {
|
||||||
DocumentsTabPrefs,
|
DocumentsTabPrefs,
|
||||||
readDocumentsTabPrefs,
|
readDocumentsTabPrefs,
|
||||||
@ -48,11 +49,11 @@ import * as DataModels from "../../../Contracts/DataModels";
|
|||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||||
import { extractPartitionKeyValues } from "../../../Utils/QueryUtils";
|
import { defaultQueryFields, extractPartitionKeyValues } from "../../../Utils/QueryUtils";
|
||||||
import DocumentId from "../../Tree/DocumentId";
|
import DocumentId from "../../Tree/DocumentId";
|
||||||
import ObjectId from "../../Tree/ObjectId";
|
import ObjectId from "../../Tree/ObjectId";
|
||||||
import TabsBase from "../TabsBase";
|
import TabsBase from "../TabsBase";
|
||||||
import { ColumnsDefinition, DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent";
|
import { ColumnDefinition, DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent";
|
||||||
|
|
||||||
export class DocumentsTabV2 extends TabsBase {
|
export class DocumentsTabV2 extends TabsBase {
|
||||||
public partitionKey: DataModels.PartitionKey;
|
public partitionKey: DataModels.PartitionKey;
|
||||||
@ -415,12 +416,19 @@ export const buildQuery = (
|
|||||||
filter: string,
|
filter: string,
|
||||||
partitionKeyProperties?: string[],
|
partitionKeyProperties?: string[],
|
||||||
partitionKey?: DataModels.PartitionKey,
|
partitionKey?: DataModels.PartitionKey,
|
||||||
|
additionalField?: string[],
|
||||||
): string => {
|
): string => {
|
||||||
if (isMongo) {
|
if (isMongo) {
|
||||||
return filter || "{}";
|
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("/")),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export to expose to unit tests
|
// Export to expose to unit tests
|
||||||
@ -542,10 +550,19 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
[partitionKeyPropertyHeaders],
|
[partitionKeyPropertyHeaders],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [selectedColumnIds, setSelectedColumnIds] = useState<string[]>(() => {
|
||||||
|
const columnsIds = ["id"];
|
||||||
|
if (showPartitionKey(_collection, isPreferredApiMongoDB)) {
|
||||||
|
columnsIds.push(...partitionKeyPropertyHeaders);
|
||||||
|
}
|
||||||
|
return columnsIds;
|
||||||
|
});
|
||||||
|
|
||||||
// new DocumentId() requires a DocumentTab which we mock with only the required properties
|
// new DocumentId() requires a DocumentTab which we mock with only the required properties
|
||||||
const newDocumentId = useCallback(
|
const newDocumentId = useCallback(
|
||||||
(rawDocument: DataModels.DocumentId, partitionKeyProperties: string[], partitionKeyValue: string[]) =>
|
(rawDocument: DataModels.DocumentId, partitionKeyProperties: string[], partitionKeyValue: string[]) => ({
|
||||||
new DocumentId(
|
...rawDocument,
|
||||||
|
...new DocumentId(
|
||||||
{
|
{
|
||||||
partitionKey,
|
partitionKey,
|
||||||
partitionKeyProperties,
|
partitionKeyProperties,
|
||||||
@ -556,6 +573,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
rawDocument,
|
rawDocument,
|
||||||
partitionKeyValue,
|
partitionKeyValue,
|
||||||
),
|
),
|
||||||
|
}),
|
||||||
[partitionKey],
|
[partitionKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -975,7 +993,13 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
const _queryAbortController = new AbortController();
|
const _queryAbortController = new AbortController();
|
||||||
setQueryAbortController(_queryAbortController);
|
setQueryAbortController(_queryAbortController);
|
||||||
const filter: string = filterContent.trim();
|
const filter: string = filterContent.trim();
|
||||||
const query: string = buildQuery(isPreferredApiMongoDB, filter, partitionKeyProperties, partitionKey);
|
const query: string = buildQuery(
|
||||||
|
isPreferredApiMongoDB,
|
||||||
|
filter,
|
||||||
|
partitionKeyProperties,
|
||||||
|
partitionKey,
|
||||||
|
selectedColumnIds,
|
||||||
|
);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const options: any = {};
|
const options: any = {};
|
||||||
// TODO: Property 'enableCrossPartitionQuery' does not exist on type 'FeedOptions'.
|
// TODO: Property 'enableCrossPartitionQuery' does not exist on type 'FeedOptions'.
|
||||||
@ -998,6 +1022,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
resourceTokenPartitionKey,
|
resourceTokenPartitionKey,
|
||||||
isQueryCopilotSampleContainer,
|
isQueryCopilotSampleContainer,
|
||||||
_collection,
|
_collection,
|
||||||
|
selectedColumnIds,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onHideFilterClick = (): void => {
|
const onHideFilterClick = (): void => {
|
||||||
@ -1030,6 +1055,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
);
|
);
|
||||||
setOnLoadStartKey(undefined);
|
setOnLoadStartKey(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update column definitions
|
||||||
};
|
};
|
||||||
|
|
||||||
let loadNextPage = useCallback(
|
let loadNextPage = useCallback(
|
||||||
@ -1194,9 +1221,35 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return { ...documentId, ...item };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const extractColumnDefinitionsFromDocument = (document: unknown): ColumnDefinition[] => {
|
||||||
|
let columnDefinitions: ColumnDefinition[] = Object.keys(document).map((key) =>
|
||||||
|
key === "id"
|
||||||
|
? { id: key, label: isPreferredApiMongoDB ? "_id" : "id", group: undefined }
|
||||||
|
: { id: key, label: key, group: undefined },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (showPartitionKey(_collection, isPreferredApiMongoDB)) {
|
||||||
|
columnDefinitions.push(
|
||||||
|
...partitionKeyPropertyHeaders.map((key) => ({ id: key, label: key, group: "Partition Key" })),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove properties that are the partition keys, since they are already included
|
||||||
|
columnDefinitions = columnDefinitions.filter(
|
||||||
|
(columnDefinition) => !partitionKeyProperties.includes(columnDefinition.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return columnDefinitions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setColumnDefinitionsFromDocument = (document: unknown): void => {
|
||||||
|
// TODO Add fields rather than replace
|
||||||
|
setColumnDefinitions(extractColumnDefinitionsFromDocument(document));
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* replicate logic of selectedDocument.click();
|
* replicate logic of selectedDocument.click();
|
||||||
* Document has been clicked on in table
|
* Document has been clicked on in table
|
||||||
@ -1212,6 +1265,9 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
(_isQueryCopilotSampleContainer ? readSampleDocument(documentId) : readDocument(_collection, documentId)).then(
|
(_isQueryCopilotSampleContainer ? readSampleDocument(documentId) : readDocument(_collection, documentId)).then(
|
||||||
(content) => {
|
(content) => {
|
||||||
initDocumentEditor(documentId, content);
|
initDocumentEditor(documentId, content);
|
||||||
|
|
||||||
|
// Update columns
|
||||||
|
setColumnDefinitionsFromDocument(content);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1302,23 +1358,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
return () => resizeObserver.disconnect(); // clean up
|
return () => resizeObserver.disconnect(); // clean up
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const columnsDefinition: ColumnsDefinition = [
|
const [columnDefinitions, setColumnDefinitions] = useState<ColumnDefinition[]>(() =>
|
||||||
{
|
extractColumnDefinitionsFromDocument({
|
||||||
id: "id",
|
id: "id",
|
||||||
label: isPreferredApiMongoDB ? "_id" : "id",
|
}),
|
||||||
defaultWidthPx: prefs.columnWidths ? prefs.columnWidths["id"] : undefined,
|
);
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (showPartitionKey(_collection, isPreferredApiMongoDB)) {
|
|
||||||
partitionKeyPropertyHeaders.forEach((header) => {
|
|
||||||
columnsDefinition.push({
|
|
||||||
id: header,
|
|
||||||
label: header,
|
|
||||||
defaultWidthPx: prefs.columnWidths ? prefs.columnWidths[header] : undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSelectedRowsChange = (selectedRows: Set<TableRowId>) => {
|
const onSelectedRowsChange = (selectedRows: Set<TableRowId>) => {
|
||||||
confirmDiscardingChange(() => {
|
confirmDiscardingChange(() => {
|
||||||
@ -1607,7 +1651,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
setIsExecuting(true);
|
setIsExecuting(true);
|
||||||
onExecutionErrorChange(false);
|
onExecutionErrorChange(false);
|
||||||
const filter: string = filterContent.trim();
|
const filter: string = filterContent.trim();
|
||||||
const query: string = buildQuery(isPreferredApiMongoDB, filter);
|
const query: string = buildQuery(isPreferredApiMongoDB, filter, selectedColumnIds);
|
||||||
|
|
||||||
return MongoProxyClient.queryDocuments(
|
return MongoProxyClient.queryDocuments(
|
||||||
_collection.databaseId,
|
_collection.databaseId,
|
||||||
@ -1697,6 +1741,31 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
setPrefs({ ...prefs });
|
setPrefs({ ...prefs });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onColumnSelectionChange = (newSelectedColumnIds: string[]): void => {
|
||||||
|
setSelectedColumnIds(newSelectedColumnIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<FluentProvider theme={getPlatformTheme(configContext.platform)} style={{ height: "100%" }}>
|
<FluentProvider theme={getPlatformTheme(configContext.platform)} style={{ height: "100%" }}>
|
||||||
<div className="tab-pane active documentsTab" role="tabpanel" style={{ display: "flex" }}>
|
<div className="tab-pane active documentsTab" role="tabpanel" style={{ display: "flex" }}>
|
||||||
@ -1840,11 +1909,13 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
onSelectedRowsChange={onSelectedRowsChange}
|
onSelectedRowsChange={onSelectedRowsChange}
|
||||||
selectedRows={selectedRows}
|
selectedRows={selectedRows}
|
||||||
size={tableContainerSizePx}
|
size={tableContainerSizePx}
|
||||||
columnsDefinition={columnsDefinition}
|
selectedColumnIds={selectedColumnIds}
|
||||||
|
columnDefinitions={columnDefinitions}
|
||||||
isSelectionDisabled={
|
isSelectionDisabled={
|
||||||
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
|
configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly
|
||||||
}
|
}
|
||||||
onColumnResize={onTableColumnResize}
|
onColumnResize={onTableColumnResize}
|
||||||
|
onColumnSelectionChange={onColumnSelectionChange}
|
||||||
/>
|
/>
|
||||||
{tableItems.length > 0 && (
|
{tableItems.length > 0 && (
|
||||||
<a
|
<a
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
|
import { SearchBox } from "@fluentui/react";
|
||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
|
MenuCheckedValueChangeData,
|
||||||
|
MenuCheckedValueChangeEvent,
|
||||||
|
MenuDivider,
|
||||||
|
MenuGroup,
|
||||||
|
MenuGroupHeader,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
MenuItemCheckbox,
|
||||||
MenuList,
|
MenuList,
|
||||||
MenuPopover,
|
MenuPopover,
|
||||||
MenuTrigger,
|
MenuTrigger,
|
||||||
@ -19,33 +26,36 @@ import {
|
|||||||
useArrowNavigationGroup,
|
useArrowNavigationGroup,
|
||||||
useTableColumnSizing_unstable,
|
useTableColumnSizing_unstable,
|
||||||
useTableFeatures,
|
useTableFeatures,
|
||||||
useTableSelection,
|
useTableSelection
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import { NormalizedEventKey } from "Common/Constants";
|
import { NormalizedEventKey } from "Common/Constants";
|
||||||
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
import { selectionHelper } from "Explorer/Tabs/DocumentsTabV2/SelectionHelper";
|
||||||
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
import { isEnvironmentCtrlPressed, isEnvironmentShiftPressed } from "Utils/KeyboardUtils";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { ChangeEvent, useCallback, useMemo } from "react";
|
||||||
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
|
import { FixedSizeList as List, ListChildComponentProps } from "react-window";
|
||||||
|
|
||||||
export type DocumentsTableComponentItem = {
|
export type DocumentsTableComponentItem = {
|
||||||
id: string;
|
id: string;
|
||||||
} & Record<string, string>;
|
} & Record<string, string>;
|
||||||
|
|
||||||
export type ColumnsDefinition = {
|
export type ColumnDefinition = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
defaultWidthPx?: number;
|
defaultWidthPx?: number;
|
||||||
}[];
|
group: string | undefined;
|
||||||
|
};
|
||||||
export interface IDocumentsTableComponentProps {
|
export interface IDocumentsTableComponentProps {
|
||||||
items: DocumentsTableComponentItem[];
|
items: DocumentsTableComponentItem[];
|
||||||
onItemClicked: (index: number) => void;
|
onItemClicked: (index: number) => void;
|
||||||
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
onSelectedRowsChange: (selectedItemsIndices: Set<TableRowId>) => void;
|
||||||
selectedRows: Set<TableRowId>;
|
selectedRows: Set<TableRowId>;
|
||||||
size: { height: number; width: number };
|
size: { height: number; width: number };
|
||||||
columnsDefinition: ColumnsDefinition;
|
selectedColumnIds: string[];
|
||||||
|
columnDefinitions: ColumnDefinition[];
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
isSelectionDisabled?: boolean;
|
isSelectionDisabled?: boolean;
|
||||||
onColumnResize?: (columnId: string, width: number) => void;
|
onColumnResize?: (columnId: string, width: number) => void;
|
||||||
|
onColumnSelectionChange?: (newSelectedColumnIds: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
|
interface TableRowData extends RowStateBase<DocumentsTableComponentItem> {
|
||||||
@ -60,6 +70,7 @@ interface ReactWindowRenderFnProps extends ListChildComponentProps {
|
|||||||
|
|
||||||
const DEFAULT_COLUMN_WIDTH_PX = 200;
|
const DEFAULT_COLUMN_WIDTH_PX = 200;
|
||||||
const MIN_COLUMN_WIDTH_PX = 20;
|
const MIN_COLUMN_WIDTH_PX = 20;
|
||||||
|
const COLUMNS_MENU_NAME = "columnsMenu";
|
||||||
|
|
||||||
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> = ({
|
||||||
items,
|
items,
|
||||||
@ -67,12 +78,14 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
selectedRows,
|
selectedRows,
|
||||||
style,
|
style,
|
||||||
size,
|
size,
|
||||||
columnsDefinition,
|
selectedColumnIds,
|
||||||
|
columnDefinitions,
|
||||||
isSelectionDisabled,
|
isSelectionDisabled,
|
||||||
onColumnResize: _onColumnResize,
|
onColumnResize: _onColumnResize,
|
||||||
|
onColumnSelectionChange,
|
||||||
}: IDocumentsTableComponentProps) => {
|
}: IDocumentsTableComponentProps) => {
|
||||||
const initialSizingOptions: TableColumnSizingOptions = {};
|
const initialSizingOptions: TableColumnSizingOptions = {};
|
||||||
columnsDefinition.forEach((column) => {
|
columnDefinitions.forEach((column) => {
|
||||||
initialSizingOptions[column.id] = {
|
initialSizingOptions[column.id] = {
|
||||||
idealWidth: column.defaultWidthPx || DEFAULT_COLUMN_WIDTH_PX, // 0 is not a valid width
|
idealWidth: column.defaultWidthPx || DEFAULT_COLUMN_WIDTH_PX, // 0 is not a valid width
|
||||||
minWidth: MIN_COLUMN_WIDTH_PX,
|
minWidth: MIN_COLUMN_WIDTH_PX,
|
||||||
@ -80,6 +93,7 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(initialSizingOptions);
|
const [columnSizingOptions, setColumnSizingOptions] = React.useState<TableColumnSizingOptions>(initialSizingOptions);
|
||||||
|
const [columnSearchText, setColumnSearchText] = React.useState<string>("");
|
||||||
|
|
||||||
const onColumnResize = React.useCallback(
|
const onColumnResize = React.useCallback(
|
||||||
(_, { columnId, width }) => {
|
(_, { columnId, width }) => {
|
||||||
@ -98,17 +112,19 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
// Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes
|
||||||
const columns: TableColumnDefinition<DocumentsTableComponentItem>[] = useMemo(
|
const columns: TableColumnDefinition<DocumentsTableComponentItem>[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
columnsDefinition.map((column) => ({
|
columnDefinitions
|
||||||
columnId: column.id,
|
.filter((column) => selectedColumnIds.includes(column.id))
|
||||||
compare: (a, b) => a[column.id].localeCompare(b[column.id]),
|
.map((column) => ({
|
||||||
renderHeaderCell: () => <span title={column.label}>{column.label}</span>,
|
columnId: column.id,
|
||||||
renderCell: (item) => (
|
compare: (a, b) => a[column.id].localeCompare(b[column.id]),
|
||||||
<TableCellLayout truncate title={item[column.id]}>
|
renderHeaderCell: () => <span title={column.label}>{column.label}</span>,
|
||||||
{item[column.id]}
|
renderCell: (item) => (
|
||||||
</TableCellLayout>
|
<TableCellLayout truncate title={item[column.id]}>
|
||||||
),
|
{item[column.id]}
|
||||||
})),
|
</TableCellLayout>
|
||||||
[columnsDefinition],
|
),
|
||||||
|
})),
|
||||||
|
[columnDefinitions, selectedColumnIds],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(undefined);
|
const [selectionStartIndex, setSelectionStartIndex] = React.useState<number>(undefined);
|
||||||
@ -250,6 +266,66 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
...style,
|
...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 onCheckedValueChange = (_: MenuCheckedValueChangeEvent, data: MenuCheckedValueChangeData) => {
|
||||||
|
// TODO this is expensive
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
|
onColumnSelectionChange(data.checkedItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearchChange: (event?: ChangeEvent<HTMLInputElement>, newValue?: string) => void = (_, newValue) =>
|
||||||
|
setColumnSearchText(newValue);
|
||||||
|
|
||||||
|
const getMenuList = (columnDefinitions: ColumnDefinition[]): JSX.Element => {
|
||||||
|
// Group by group. Unnamed group first
|
||||||
|
const unnamedGroup: ColumnDefinition[] = [];
|
||||||
|
const groupMap = new Map<string, ColumnDefinition[]>();
|
||||||
|
columnDefinitions.forEach((column) => {
|
||||||
|
if (column.group) {
|
||||||
|
if (!groupMap.has(column.group)) {
|
||||||
|
groupMap.set(column.group, []);
|
||||||
|
}
|
||||||
|
groupMap.get(column.group).push(column);
|
||||||
|
} else {
|
||||||
|
unnamedGroup.push(column);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuList: JSX.Element[] = [];
|
||||||
|
menuList.push(<SearchBox key="search" value={columnSearchText} onChange={onSearchChange} />)
|
||||||
|
if (unnamedGroup.length > 0) {
|
||||||
|
menuList.push(
|
||||||
|
...unnamedGroup.filter(def => !columnSearchText || def.label.startsWith(columnSearchText)).map((column) => (
|
||||||
|
<MenuItemCheckbox key={column.id} name={COLUMNS_MENU_NAME} value={column.id}>
|
||||||
|
{column.label}
|
||||||
|
</MenuItemCheckbox>
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
groupMap.forEach((columns, group) => {
|
||||||
|
menuList.push(<MenuDivider key={`divider${group}`} />);
|
||||||
|
menuList.push(
|
||||||
|
<MenuGroup key={group}>
|
||||||
|
<MenuGroupHeader>{group}</MenuGroupHeader>
|
||||||
|
{...columns.map((column) => (
|
||||||
|
<MenuItemCheckbox key={column.id} name={COLUMNS_MENU_NAME} value={column.id}>
|
||||||
|
{column.label}
|
||||||
|
</MenuItemCheckbox>
|
||||||
|
))}
|
||||||
|
</MenuGroup>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <>{menuList}</>;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className="documentsTable" noNativeElements {...tableProps}>
|
<Table className="documentsTable" noNativeElements {...tableProps}>
|
||||||
<TableHeader className="documentsTableHeader">
|
<TableHeader className="documentsTableHeader">
|
||||||
@ -263,7 +339,12 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{columns.map((column /* index */) => (
|
{columns.map((column /* index */) => (
|
||||||
<Menu openOnContext key={column.columnId}>
|
<Menu
|
||||||
|
openOnContext
|
||||||
|
key={column.columnId}
|
||||||
|
checkedValues={checkedValues}
|
||||||
|
onCheckedValueChange={onCheckedValueChange}
|
||||||
|
>
|
||||||
<MenuTrigger>
|
<MenuTrigger>
|
||||||
<TableHeaderCell
|
<TableHeaderCell
|
||||||
className="documentsTableCell"
|
className="documentsTableCell"
|
||||||
@ -274,9 +355,11 @@ export const DocumentsTableComponent: React.FC<IDocumentsTableComponentProps> =
|
|||||||
</TableHeaderCell>
|
</TableHeaderCell>
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
<MenuPopover>
|
<MenuPopover>
|
||||||
<MenuList>
|
<MenuList style={{ maxHeight: size?.height, overflowY: "auto", overflowX: "hidden" }}>
|
||||||
|
{getMenuList(columnDefinitions)}
|
||||||
|
<MenuDivider />
|
||||||
<MenuItem onClick={columnSizing.enableKeyboardMode(column.columnId)}>
|
<MenuItem onClick={columnSizing.enableKeyboardMode(column.columnId)}>
|
||||||
Keyboard Column Resizing
|
Use Left/Right Arrow keys to resize
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</MenuPopover>
|
</MenuPopover>
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class to help with selection.
|
* Utility class to help with selection.
|
||||||
* This emulates File Explorer selection behavior.
|
* This emulates File Explorer selection behavior.
|
||||||
@ -90,3 +92,12 @@ export const selectionHelper = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// To get previous values of a state in useEffect
|
||||||
|
export const usePrevious = <T>(value: T): T | undefined => {
|
||||||
|
const ref = useRef<T>();
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
});
|
||||||
|
return ref.current;
|
||||||
|
};
|
||||||
|
@ -4,7 +4,7 @@ import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
|||||||
|
|
||||||
export interface DocumentsTabPrefs {
|
export interface DocumentsTabPrefs {
|
||||||
leftPaneWidthPercent: number;
|
leftPaneWidthPercent: number;
|
||||||
columnWidths?: { [columnId: string]: number };
|
columnWidths?: { [columnId: string]: number }; // TODO save per database/collection
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultPrefs: DocumentsTabPrefs = {
|
const defaultPrefs: DocumentsTabPrefs = {
|
||||||
|
@ -2,18 +2,28 @@ import { PartitionKey, PartitionKeyDefinition } from "@azure/cosmos";
|
|||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
|
||||||
|
export const defaultQueryFields = ["id", "_self", "_rid", "_ts"];
|
||||||
|
|
||||||
export function buildDocumentsQuery(
|
export function buildDocumentsQuery(
|
||||||
filter: string,
|
filter: string,
|
||||||
partitionKeyProperties: string[],
|
partitionKeyProperties: string[],
|
||||||
partitionKey: DataModels.PartitionKey,
|
partitionKey: DataModels.PartitionKey,
|
||||||
|
additionalField: string[] = [],
|
||||||
): string {
|
): string {
|
||||||
|
const fieldSet = new Set<string>(defaultQueryFields);
|
||||||
|
additionalField.forEach((prop) => fieldSet.add(prop));
|
||||||
|
|
||||||
|
const objectListSpec = [...fieldSet]
|
||||||
|
.filter((f) => !partitionKeyProperties.includes(f))
|
||||||
|
.map((prop) => `c.${prop}`)
|
||||||
|
.join(",");
|
||||||
let query =
|
let query =
|
||||||
partitionKeyProperties && partitionKeyProperties.length > 0
|
partitionKeyProperties && partitionKeyProperties.length > 0
|
||||||
? `select c.id, c._self, c._rid, c._ts, [${buildDocumentsQueryPartitionProjections(
|
? `select ${objectListSpec}, [${buildDocumentsQueryPartitionProjections(
|
||||||
"c",
|
"c",
|
||||||
partitionKey,
|
partitionKey,
|
||||||
)}] as _partitionKeyValue from c`
|
)}] as _partitionKeyValue from c`
|
||||||
: `select c.id, c._self, c._rid, c._ts from c`;
|
: `select ${objectListSpec} from c`;
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
query += " " + filter;
|
query += " " + filter;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user