import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Body,
Button,
Checkbox,
Disclaimer,
ErrorSummary,
Icon,
Placeholder,
Table,
TableHeader,
Row,
Cell,
TextInput,
css,
spacing,
Label,
} from '@mongodb-js/compass-components';
import { connect } from 'react-redux';
import {
selectFieldsToExport,
toggleFieldToExport,
addFieldToExport,
toggleExportAllSelectedFields,
getIdForSchemaPath,
} from '../modules/export';
import type { FieldsToExport } from '../modules/export';
import type { RootExportState } from '../stores/export-store';
import type { SchemaPath } from '../export/gather-fields';
const headerContainerStyles = css({
display: 'flex',
flexDirection: 'column',
marginBottom: spacing[3],
marginTop: spacing[2],
gap: spacing[2],
});
const tableContainerStyles = css({
maxHeight: spacing[7] * 3,
overflow: 'auto',
});
const smallCellContainerStyle = css({
width: spacing[5],
margin: '0 auto',
});
const textInputStyles = css({
padding: `${spacing[1]}px 0`,
minWidth: spacing[7] * 3,
});
const enterToAddStyles = css({
display: 'inline-flex',
alignItems: 'center',
marginLeft: spacing[2],
});
const placeholderStyles = css({
margin: spacing[1],
});
const retryButtonContainerStyles = css({
margin: spacing[2],
textAlign: 'center',
});
const addNewFieldRowStyles = css({
marginBottom: spacing[5],
});
const loadingPlaceholderCount = 6;
const loadingPlaceholderItems = Array.from({
length: loadingPlaceholderCount,
}).map((value, index) => index);
const MAX_FIELDS_TO_SHOW_DEFAULT = 100;
const FIELDS_TO_SHOW_INCREASE = 100;
function LoadingTable() {
return (
{
/* noop */
}}
/>
}
/>,
,
]}
>
{({ datum: index }) => (
|
|
)}
);
}
type ExportSelectFieldsProps = {
isLoading: boolean;
errorLoadingFieldsToExport?: string;
fields: FieldsToExport;
selectFieldsToExport: () => void; // Used to retry fetching fields when fetching the fields fails.
addFieldToExport: (path: SchemaPath) => void;
toggleFieldToExport: (fieldId: string, selected: boolean) => void;
toggleExportAllSelectedFields: () => void;
};
function ExportSelectFields({
errorLoadingFieldsToExport,
isLoading,
fields,
addFieldToExport,
selectFieldsToExport,
toggleFieldToExport,
toggleExportAllSelectedFields,
}: ExportSelectFieldsProps) {
const newFieldRef = useRef(null);
const [maxFieldsToShow, setMaxFieldsToShow] = useState(
MAX_FIELDS_TO_SHOW_DEFAULT
);
// Track the fields length so we know when to auto-focus
// the add field input when a new field is added.
const lastRenderedFieldsLength = useRef(0);
const [autoScrollNewFieldInput, setAutoScrollNewFieldInput] = useState(false);
const fieldKeys = useMemo(() => Object.keys(fields), [fields]);
const isEveryFieldChecked = useMemo(() => {
return Object.keys(fields).every((f) => fields[f].selected);
}, [fields]);
const onAddNewFieldButtonClicked = useCallback(() => {
if (newFieldRef.current) {
newFieldRef.current.scrollIntoView();
newFieldRef.current.focus();
}
}, []);
const handleFieldCheckboxChange = useCallback(
(evt: React.ChangeEvent) => {
toggleFieldToExport(
`${evt.target.name}`,
!fields[`${evt.target.name}`].selected
);
},
[toggleFieldToExport, fields]
);
const handleAddFieldSubmit = useCallback(
(evt) => {
if (evt.key === 'Enter') {
addFieldToExport(evt.target.value.split('.') as SchemaPath);
}
},
[addFieldToExport]
);
useEffect(() => {
if (!autoScrollNewFieldInput) {
return;
}
if (newFieldRef.current) {
// Focus and scroll to the add new field input.
newFieldRef.current.scrollIntoView();
newFieldRef.current.focus();
}
setAutoScrollNewFieldInput(false);
}, [autoScrollNewFieldInput]);
useEffect(() => {
if (
lastRenderedFieldsLength.current !== 0 &&
fieldKeys.length > lastRenderedFieldsLength.current
) {
setAutoScrollNewFieldInput(true);
}
lastRenderedFieldsLength.current = fieldKeys.length;
}, [fieldKeys]);
const { fieldsToRender, hasMoreFieldsToShow } = useMemo(() => {
let fieldsShown = 0;
let hasMoreFieldsToShow = false;
const fieldsToRender = fieldKeys
.filter(
// When a key has a parent that is already checked it will
// already be included in the projection, so we hide them.
(fieldKey) => {
const path: SchemaPath = [];
if (hasMoreFieldsToShow && fieldsShown >= maxFieldsToShow) {
// We limit the amount of fields shown so that
// we don't freeze when rendering a lot of fields.
return false;
}
for (const fieldName of fields[fieldKey].path) {
path.push(fieldName);
const fieldId = getIdForSchemaPath(path);
if (fields[fieldId]?.selected && fieldId !== fieldKey) {
return false;
}
}
if (fieldsShown >= maxFieldsToShow) {
hasMoreFieldsToShow = true;
return false;
}
fieldsShown++;
return true;
}
)
.map((fieldKey, index) => ({
fieldKey,
fieldLabel: fields[fieldKey].path.join('.'),
checked: !!fields[fieldKey].selected,
index,
}));
return {
fieldsToRender,
hasMoreFieldsToShow,
};
}, [fields, fieldKeys, maxFieldsToShow]);
return (
<>
Select Fields
The fields in the table below are from a
sample of documents in
the collection. Add missing fields you want to export.
}
data-testid="export-add-new-field-button"
size="xsmall"
disabled={isLoading}
onClick={onAddNewFieldButtonClicked}
>
Add new field
{isLoading ? (
) : (
}
/>,
,
]}
>
{({ datum: field }) => (
<>
|
|
|
{field.index === fieldsToRender.length - 1 && (
<>
{hasMoreFieldsToShow && (
|
|
|
)}
|
|
Press "Enter" to add field
|
>
)}
>
)}
)}
{!!errorLoadingFieldsToExport && (
)}
>
);
}
const ConnectedExportSelectFields = connect(
(state: RootExportState) => ({
errorLoadingFieldsToExport: state.export.errorLoadingFieldsToExport,
fields: state.export.fieldsToExport,
isLoading: !!state.export.fieldsToExportAbortController,
}),
{
selectFieldsToExport,
addFieldToExport,
toggleFieldToExport,
toggleExportAllSelectedFields,
}
)(ExportSelectFields);
export {
ConnectedExportSelectFields as ExportSelectFields,
ExportSelectFields as UnconnectedExportSelectFields,
};