From f49e392ad332f4974e0ad8ca89368802abfe4d51 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Wed, 12 Feb 2025 11:45:41 -0500 Subject: [PATCH] Better handling on entity create/edit edge cases (#1455) * I think this is much safer and smarter. If we don't have schema we should not assume things can be empty * Updating material input control to handle disabled inputs Switching to using custom input control * We do not want to allow editing of this field and have users only select the connector on the connector select page. * Updating content since the field is not editable * Since we do not allow editing of connector we want them to be able to go back * Moving create routes into the entity settings * Renaming to make it more appropriate * Hiding iframe unless it is being shown Adding eventing to keep an eye on this --- .../Entity/DetailsForm/useConnectorField.ts | 3 ++ .../Entity/hooks/useEntityCreateNavigate.ts | 20 ++------ src/components/sidePanelDocs/Iframe.tsx | 2 +- src/components/sidePanelDocs/OpenButton.tsx | 5 ++ src/components/sidePanelDocs/SidePanel.tsx | 5 ++ src/forms/renderers/Connectors.tsx | 7 +-- src/forms/renderers/DataPlanes.tsx | 7 +-- .../controls/MaterialInputControl.tsx | 47 ++++++++++++------- src/lang/en-US/CommonMessages.ts | 2 +- src/services/types.ts | 1 + src/settings/entity.ts | 8 ++++ src/settings/types.ts | 2 + src/utils/misc-utils.ts | 6 ++- 13 files changed, 75 insertions(+), 40 deletions(-) diff --git a/src/components/shared/Entity/DetailsForm/useConnectorField.ts b/src/components/shared/Entity/DetailsForm/useConnectorField.ts index 495850481..3a36bb165 100644 --- a/src/components/shared/Entity/DetailsForm/useConnectorField.ts +++ b/src/components/shared/Entity/DetailsForm/useConnectorField.ts @@ -113,6 +113,9 @@ export default function useConnectorField( }), scope: `#/properties/${CONNECTOR_IMAGE_SCOPE}`, type: 'Control', + options: { + readOnly: true, + }, }; const evaluateConnector = useCallback( diff --git a/src/components/shared/Entity/hooks/useEntityCreateNavigate.ts b/src/components/shared/Entity/hooks/useEntityCreateNavigate.ts index 5ef0a8c1f..67b354137 100644 --- a/src/components/shared/Entity/hooks/useEntityCreateNavigate.ts +++ b/src/components/shared/Entity/hooks/useEntityCreateNavigate.ts @@ -1,9 +1,9 @@ -import { authenticatedRoutes } from 'app/routes'; import { GlobalSearchParams } from 'hooks/searchParams/useGlobalSearchParams'; import useSearchParamAppend from 'hooks/searchParams/useSearchParamAppend'; import { isEmpty } from 'lodash'; import { useCallback } from 'react'; import { useNavigate } from 'react-router'; +import { ENTITY_SETTINGS } from 'settings/entity'; import { EntityWithCreateWorkflow } from 'types'; import { getPathWithParams, hasLength } from 'utils/misc-utils'; @@ -41,24 +41,14 @@ export default function useEntityCreateNavigate() { ? appendSearchParams(searchParamConfig) : null; - let newPath: string | null = null; - if (entity === 'capture') { - newPath = advanceToForm - ? authenticatedRoutes.captures.create.new.fullPath - : authenticatedRoutes.captures.create.fullPath; - } else { - newPath = advanceToForm - ? authenticatedRoutes.materializations.create.new.fullPath - : authenticatedRoutes.materializations.create.fullPath; - } + const newPath: string = advanceToForm + ? ENTITY_SETTINGS[entity].routes.createNew + : ENTITY_SETTINGS[entity].routes.connectorSelect; navigate( newSearchParams ? getPathWithParams(newPath, newSearchParams) - : newPath, - { - replace, - } + : newPath ); }, [appendSearchParams, navigate] diff --git a/src/components/sidePanelDocs/Iframe.tsx b/src/components/sidePanelDocs/Iframe.tsx index c19695f30..0d473fed2 100644 --- a/src/components/sidePanelDocs/Iframe.tsx +++ b/src/components/sidePanelDocs/Iframe.tsx @@ -72,7 +72,7 @@ function SidePanelIframe({ show }: Props) { }, [docsURL, iframeCurrent, setAnimateOpening, show]); // Make sure we don't include an iframe unless we actually need it - if (!hasLength(docsURL)) { + if (!hasLength(docsURL) || !show) { return null; } diff --git a/src/components/sidePanelDocs/OpenButton.tsx b/src/components/sidePanelDocs/OpenButton.tsx index 249c54ca2..0c0c2621a 100644 --- a/src/components/sidePanelDocs/OpenButton.tsx +++ b/src/components/sidePanelDocs/OpenButton.tsx @@ -10,6 +10,8 @@ import { import { useShowSidePanelDocs } from 'context/SidePanelDocs'; import { SidebarCollapse } from 'iconoir-react'; import { FormattedMessage } from 'react-intl'; +import { logRocketEvent } from 'services/shared'; +import { CustomEvents } from 'services/types'; import { useSidePanelDocsStore } from 'stores/SidePanelDocs/Store'; import { hasLength } from 'utils/misc-utils'; @@ -40,6 +42,9 @@ function SidePanelDocsOpenButton() { size="small" variant="outlined" onClick={() => { + logRocketEvent(CustomEvents.HELP_DOCS, { + show: true, + }); setShowDocs(true); }} endIcon={ diff --git a/src/components/sidePanelDocs/SidePanel.tsx b/src/components/sidePanelDocs/SidePanel.tsx index e82f9753c..dec1c3127 100644 --- a/src/components/sidePanelDocs/SidePanel.tsx +++ b/src/components/sidePanelDocs/SidePanel.tsx @@ -2,6 +2,8 @@ import { Drawer, IconButton, Toolbar, Typography } from '@mui/material'; import { useShowSidePanelDocs } from 'context/SidePanelDocs'; import { Xmark } from 'iconoir-react'; import { FormattedMessage } from 'react-intl'; +import { logRocketEvent } from 'services/shared'; +import { CustomEvents } from 'services/types'; import SidePanelIframe from './Iframe'; interface Props { @@ -42,6 +44,9 @@ function DocsSidePanel({ show }: Props) { { + logRocketEvent(CustomEvents.HELP_DOCS, { + show: false, + }); setShowDocs(false); }} sx={{ color: (theme) => theme.palette.text.primary }} diff --git a/src/forms/renderers/Connectors.tsx b/src/forms/renderers/Connectors.tsx index 10b5e457e..c3c05e1ed 100644 --- a/src/forms/renderers/Connectors.tsx +++ b/src/forms/renderers/Connectors.tsx @@ -32,10 +32,10 @@ import { rankWith, scopeEndsWith, } from '@jsonforms/core'; -import { MaterialInputControl } from '@jsonforms/material-renderers'; import { WithOptionLabel } from '@jsonforms/material-renderers/lib/mui-controls/MuiAutocomplete'; import { withJsonFormsOneOfEnumProps } from '@jsonforms/react'; import { ConnectorAutoComplete } from 'forms/renderers/ConnectorSelect/AutoComplete'; +import { CustomMaterialInputControl } from './Overrides/material/controls/MaterialInputControl'; export const CONNECTOR_IMAGE_SCOPE = 'connectorImage'; @@ -44,11 +44,12 @@ export const connectorTypeTester: RankedTester = rankWith( and(isOneOfEnumControl, scopeEndsWith(CONNECTOR_IMAGE_SCOPE)) ); -// This is blank on purpose. For right now we can just show null settings are nothing const ConnectorTypeRenderer = ( props: ControlProps & OwnPropsOfEnum & WithOptionLabel ) => { - return ; + return ( + + ); }; export const ConnectorType = withJsonFormsOneOfEnumProps(ConnectorTypeRenderer); diff --git a/src/forms/renderers/DataPlanes.tsx b/src/forms/renderers/DataPlanes.tsx index 720227f2f..7438bd373 100644 --- a/src/forms/renderers/DataPlanes.tsx +++ b/src/forms/renderers/DataPlanes.tsx @@ -32,10 +32,10 @@ import { rankWith, scopeEndsWith, } from '@jsonforms/core'; -import { MaterialInputControl } from '@jsonforms/material-renderers'; import { WithOptionLabel } from '@jsonforms/material-renderers/lib/mui-controls/MuiAutocomplete'; import { withJsonFormsOneOfEnumProps } from '@jsonforms/react'; import { DataPlaneAutoComplete } from './DataPlaneSelector/AutoComplete'; +import { CustomMaterialInputControl } from './Overrides/material/controls/MaterialInputControl'; export const DATA_PLANE_SCOPE = 'dataPlane'; @@ -44,11 +44,12 @@ export const dataPlaneTester: RankedTester = rankWith( and(isOneOfEnumControl, scopeEndsWith(DATA_PLANE_SCOPE)) ); -// This is blank on purpose. For right now we can just show null settings are nothing const DataPlaneRenderer = ( props: ControlProps & OwnPropsOfEnum & WithOptionLabel ) => { - return ; + return ( + + ); }; export const DataPlane = withJsonFormsOneOfEnumProps(DataPlaneRenderer); diff --git a/src/forms/renderers/Overrides/material/controls/MaterialInputControl.tsx b/src/forms/renderers/Overrides/material/controls/MaterialInputControl.tsx index 1f2441786..79daa069a 100644 --- a/src/forms/renderers/Overrides/material/controls/MaterialInputControl.tsx +++ b/src/forms/renderers/Overrides/material/controls/MaterialInputControl.tsx @@ -40,17 +40,17 @@ export interface WithInput { } interface Props { - inputEvents: { + inputEvents?: { keyDown: (event?: any) => any; focus: (event?: any) => any; }; } -// ONLY USE THIS WHEN YOU NEED TO CONTROL FOCUS. // Customizations: // 1. inputEvents // Allows you to pass in a focus function that fires when the input is focused -// +// 2. FormControl disable-able : +// This uses the `enabled` flag to set `disable` on form control so labels and helper text show as disabled export const CustomMaterialInputControl = ( props: Props & ControlProps & WithInput ) => { @@ -58,6 +58,7 @@ export const CustomMaterialInputControl = ( const { id, description, + enabled, errors, label, uischema, @@ -88,24 +89,38 @@ export const CustomMaterialInputControl = ( return ( { - if (endsWith(event.target.id, CLEAR_BUTTON_ID_SUFFIX)) { - // Clear button was clicked so we do not want to fire the focus event - // Return here so we do not fire the focus events. This way when a user - // clicks on the reset button the date picker is not opened right up - return; - } + onFocus={ + inputEvents + ? (event) => { + if ( + endsWith( + event.target.id, + CLEAR_BUTTON_ID_SUFFIX + ) + ) { + // Clear button was clicked so we do not want to fire the focus event + // Return here so we do not fire the focus events. This way when a user + // clicks on the reset button the date picker is not opened right up + return; + } - inputEvents.focus(); - onFocus(); - }} - onKeyDown={() => { - inputEvents.keyDown(); - }} + inputEvents.focus(); + onFocus(); + } + : undefined + } + onKeyDown={ + inputEvents + ? () => { + inputEvents.keyDown(); + } + : undefined + } > = { 'entityPrefix.description': `Prefix for the entity name.`, 'entityName.label': `Name`, 'connector.label': `Connector`, - 'connector.description': `Choose the external system you're connecting to.`, + 'connector.description': `The external system you're connecting to.`, 'description.label': `Details`, 'description.description': `Describe your changes or why you're changing things.`, diff --git a/src/services/types.ts b/src/services/types.ts index 9598601e0..5cdfb9a70 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -35,6 +35,7 @@ export enum CustomEvents { FIELD_SELECTION_REFRESH_MANUAL = 'Field_Selection_Refresh:Manual', FORM_STATE_PREVENTED = 'FormState:Prevented', FULL_PAGE_ERROR_DISPLAYED = 'Full_Page_Error_Displayed', + HELP_DOCS = 'Help_Docs', INCOMPATIBLE_SCHEMA_CHANGE = 'IncompatibleSchemaChange', LAZY_LOADING = 'Lazy Loading', LOGIN = 'Login', diff --git a/src/settings/entity.ts b/src/settings/entity.ts index 232d557ae..2b72903fe 100644 --- a/src/settings/entity.ts +++ b/src/settings/entity.ts @@ -15,6 +15,8 @@ export const ENTITY_SETTINGS: { [k in Entity]: EntitySetting } = { bindingTermId: 'terms.bindings.plural', pluralId: 'terms.sources.plural', routes: { + connectorSelect: authenticatedRoutes.captures.create.fullPath, + createNew: authenticatedRoutes.captures.create.new.fullPath, details: authenticatedRoutes.captures.details.overview.fullPath, viewAll: authenticatedRoutes.captures.fullPath, }, @@ -44,6 +46,8 @@ export const ENTITY_SETTINGS: { [k in Entity]: EntitySetting } = { bindingTermId: 'terms.collections.plural', pluralId: 'terms.collections.plural', routes: { + connectorSelect: authenticatedRoutes.collections.create.fullPath, + createNew: authenticatedRoutes.collections.create.new.fullPath, details: authenticatedRoutes.collections.details.overview.fullPath, viewAll: authenticatedRoutes.collections.fullPath, }, @@ -71,6 +75,10 @@ export const ENTITY_SETTINGS: { [k in Entity]: EntitySetting } = { bindingTermId: 'terms.collections.plural', pluralId: 'terms.destinations.plural', routes: { + connectorSelect: + authenticatedRoutes.materializations.create.fullPath, + createNew: authenticatedRoutes.materializations.create.new.fullPath, + details: authenticatedRoutes.materializations.details.overview.fullPath, viewAll: authenticatedRoutes.materializations.fullPath, diff --git a/src/settings/types.ts b/src/settings/types.ts index 4e1163cd5..f1e8c0b8d 100644 --- a/src/settings/types.ts +++ b/src/settings/types.ts @@ -20,6 +20,8 @@ export interface EntitySetting { bindingTermId: string; pluralId: string; routes: { + connectorSelect: string; + createNew: string; details: string; viewAll: string; }; diff --git a/src/utils/misc-utils.ts b/src/utils/misc-utils.ts index 9dfb86c1a..824fd133d 100644 --- a/src/utils/misc-utils.ts +++ b/src/utils/misc-utils.ts @@ -203,7 +203,11 @@ export const getDereffedSchema = async (val: any) => { }; export const configCanBeEmpty = (schema: any) => { - return Boolean(!schema?.properties || isEmpty(schema?.properties)); + if (!schema) { + return false; + } + + return Boolean(!schema.properties || isEmpty(schema.properties)); }; export const isReactElement = (value: ReactNode): value is ReactElement =>