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.
{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, };