Skip to content

Commit

Permalink
[UI] fixes and improvements (#253)
Browse files Browse the repository at this point in the history
  • Loading branch information
brookewp authored Dec 18, 2024
1 parent fdf9549 commit d106db9
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 49 deletions.
55 changes: 45 additions & 10 deletions src/data-sources/DataSourceSettings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@media screen and (min-width: 782px) {
height: calc(100vh - 132px);
display: grid;
grid-template-rows: 1fr auto;
}
}

Expand Down Expand Up @@ -134,17 +135,21 @@
gap: 12px;
}

.rdb-settings-page_data-source-form-uuid-error {
color: var(--Gutenberg-Alert-Red, #d94f4f);
font-size: var(--s, 12px);
line-height: var(--xs, 16px);
margin-top: 4px;
}
.rdb-settings-page_data-source-form-input.has-error {

.components-input-control__container .components-input-control__input:not(:focus) {
border-radius: var(--radius-s, 2px);
outline: 2px solid var(--Gutenberg-Alert-Red, #cc1818);
outline-offset: -2px;

}

.rdb-settings-page_data-source-form-uuid-input .components-input-control__input:focus,
.rdb-settings-page_data-source-form-uuid-input .components-input-control__container .components-input-control__backdrop:focus {
border-color: var(--wp-components-color-accent, var(--Gutenberg-Alert-Red, #d94f4f)) !important;
box-shadow: 0 0 0 0.5px var(--wp-components-color-accent, var(--Gutenberg-Alert-Red, #d94f4f)) !important;
span {
color: var(--Gutenberg-Alert-Red, #cc1818);
font-size: var(--s, 12px);
line-height: var(--xs, 16px);
margin-top: 4px;
}
}

.rdb-settings-page_data-source-multi-step-form,
Expand Down Expand Up @@ -220,3 +225,33 @@
}
}
}

.components-form-token-field__input-container.is-active {
position: relative;
}

/* Default dropdown styling */
.components-form-token-field__suggestions-list {
position: absolute;
top: 100%;
left: -1px;
background: #fff;
border: 1px solid var(--wp-admin-theme-color);
border-radius: 2px;
box-shadow: 0 0 0 0.5px var(--wp-admin-theme-color);
outline: 2px solid transparent;
}

/* Position dropdown below the input */
.form-token-field-wrapper.below .components-form-token-field__suggestions-list {
top: 100%;
bottom: auto;

}

/* Position dropdown above the input */
.form-token-field-wrapper.above .components-form-token-field__suggestions-list {
bottom: 100%;
top: auto;

}
15 changes: 11 additions & 4 deletions src/data-sources/airtable/AirtableSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { FormTokenField, SelectControl, Spinner } from '@wordpress/components';
import { SelectControl, Spinner } from '@wordpress/components';
import { InputChangeCallback } from '@wordpress/components/build-types/input-control/types';
import { useEffect, useMemo, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { ChangeEvent } from 'react';

import { CustomFormFieldToken } from '../components/CustomFormFieldToken';
import { SUPPORTED_AIRTABLE_TYPES } from '@/data-sources/airtable/constants';
import { AirtableFormState } from '@/data-sources/airtable/types';
import { getAirtableOutputQueryMappingValue } from '@/data-sources/airtable/utils';
Expand Down Expand Up @@ -240,8 +241,8 @@ export const AirtableSettings = ( {

if ( selectedTable ) {
return sprintf(
__( 'Fields: %s', 'remote-data-blocks' ),
selectedTable.fields.map( field => field.name ).join( ', ' )
__( '%s fields found', 'remote-data-blocks' ),
selectedTable.fields.length
);
}
}
Expand Down Expand Up @@ -350,7 +351,7 @@ export const AirtableSettings = ( {
/>

{ state.table && availableTableFields.length ? (
<FormTokenField
<CustomFormFieldToken
label={ __( 'Fields', 'remote-data-blocks' ) }
onChange={ selection => {
if ( selection.includes( 'Select All' ) ) {
Expand All @@ -373,8 +374,14 @@ export const AirtableSettings = ( {
...availableTableFields,
] }
value={ Array.from( state.table_fields ) }
__experimentalValidateInput={ input =>
availableTableFields.includes( input ) ||
input === 'Select All' ||
input === 'Deselect All'
}
__nextHasNoMarginBottom
__experimentalExpandOnFocus
__next40pxDefaultSize
/>
) : (
state.table && <Spinner />
Expand Down
42 changes: 42 additions & 0 deletions src/data-sources/components/CustomFormFieldToken.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { FormTokenField } from '@wordpress/components';
import { FormTokenFieldProps } from '@wordpress/components/build-types/form-token-field/types';
import { useEffect, useRef, useState } from '@wordpress/element';

type CustomFormFieldTokenProps = FormTokenFieldProps;

export const CustomFormFieldToken = ( props: CustomFormFieldTokenProps ) => {
const [ isAbove, setIsAbove ] = useState( false );
const inputRef = useRef( null );

const handlePosition = () => {
if ( ! inputRef.current ) return;

const { bottom, top } = ( inputRef.current as HTMLElement ).getBoundingClientRect();
const viewportHeight = window.innerHeight;
const dropdownHeight = 200;

// Determine whether there is more space above or below
setIsAbove( viewportHeight - bottom < dropdownHeight && top > dropdownHeight );
};

useEffect( () => {
const handleResize = () => handlePosition(); // Ensures stable reference for cleanup

handlePosition(); // Calculate position on mount
window.addEventListener( 'resize', handleResize ); // Recalculate on window resize

return () => {
window.removeEventListener( 'resize', handleResize );
};
}, [] );

return (
<div
ref={ inputRef }
className={ `form-token-field-wrapper ${ isAbove ? 'above' : 'below' }` }
style={ { position: 'relative' } }
>
<FormTokenField { ...props } />
</div>
);
};
117 changes: 88 additions & 29 deletions src/data-sources/components/DataSourceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import {
__experimentalInputControlPrefixWrapper as InputControlPrefixWrapper,
__experimentalSpacer as Spacer,
} from '@wordpress/components';
import { Children, createPortal, isValidElement, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Children, createPortal, isValidElement, useEffect, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { lockSmall } from '@wordpress/icons';

import { DataSourceFormActions } from './DataSourceFormActions';
import { useDataSources } from '../hooks/useDataSources';
import { ModalWithButtonTrigger } from '@/blocks/remote-data-container/components/modals/BaseModal';
import { useModalState } from '@/blocks/remote-data-container/hooks/useModalState';
import { useSettingsContext } from '@/settings/hooks/useSettingsNav';
Expand Down Expand Up @@ -68,16 +69,29 @@ const DataSourceForm = ( { children, onSave }: DataSourceFormProps ) => {
const [ currentStep, setCurrentStep ] = useState( 1 );
const { goToMainScreen, screen } = useSettingsContext();

const { checkDisplayNameConflict } = useDataSources();

const steps = Children.toArray( children );
const singleStep = steps.length === 1 || screen === 'editDataSource';

const stepHeadings = [ 'Setup', 'Scope' ];

const canProceedToNextStep = (): boolean => {
const step = steps[ currentStep - 1 ];
return isValidElement< { canProceed?: boolean } >( step )
? Boolean( step.props?.canProceed )
: false;
if (
isValidElement< { canProceed?: boolean; displayName: string; uuidFromProps: string } >( step )
) {
const { displayName, uuidFromProps } = step.props;

if ( currentStep === 1 ) {
return (
Boolean( step.props?.canProceed ) &&
checkDisplayNameConflict( displayName, uuidFromProps )
);
}
return Boolean( step.props?.canProceed );
}
return false;
};

const handleNextStep = () => {
Expand Down Expand Up @@ -119,7 +133,10 @@ const DataSourceForm = ( { children, onSave }: DataSourceFormProps ) => {
{ screen === 'editDataSource' && (
<>
{ createPortal(
<DataSourceFormActions onSave={ onSave } isSaveDisabled={ false } />,
<DataSourceFormActions
onSave={ onSave }
isSaveDisabled={ ! canProceedToNextStep() }
/>,
document.getElementById( 'rdb-settings-page-form-save-button' ) ||
document.createElement( 'div' )
) }
Expand Down Expand Up @@ -187,6 +204,7 @@ const DataSourceForm = ( { children, onSave }: DataSourceFormProps ) => {

const DataSourceFormSetup = ( {
children,
canProceed,
displayName: initialDisplayName,
handleOnChange,
heading,
Expand All @@ -195,13 +213,17 @@ const DataSourceFormSetup = ( {
setNewUUID,
uuidFromProps,
}: DataSourceFormSetupProps ) => {
const [ displayName, setDisplayName ] = useState( initialDisplayName );
const { close, isOpen, open } = useModalState();

const { screen, service } = useSettingsContext();
const { checkDisplayNameConflict } = useDataSources();

const [ displayName, setDisplayName ] = useState( initialDisplayName );
const [ errors, setErrors ] = useState< Record< string, string > >( {} );

const { icon, height, label, width, verticalAlign } = heading;

const onDisplayNameChange = ( displayNameInput: string | undefined ) => {
setErrors( {} );
const sanitizedDisplayName = displayNameInput
?.toString()
.trim()
Expand All @@ -215,16 +237,33 @@ const DataSourceFormSetup = ( {
return uuidv4Regex.test( uuid );
};

const defaultUUIDHelpText = (
<>
{ __(
'Unique identifier allows you to reference this data source in code.',
'remote-data-blocks'
) }
</>
);
const validateDisplayName = () => {
const hasConflict = ! checkDisplayNameConflict( displayName, uuidFromProps ?? '' );

const [ UUIDHelpText, setUUIDHelpText ] = useState( defaultUUIDHelpText );
if ( ! displayName.trim() ) {
setErrors( {
displayName: __( 'Please provide a name for your data source.', 'remote-data-blocks' ),
} );
} else if ( hasConflict ) {
setErrors( {
displayName: sprintf(
__(
'Data source "%s" already exists. Please choose another name.',
'remote-data-blocks'
),
displayName
),
} );
} else {
setErrors( {} );
}
};

useEffect( () => {
if ( canProceed ) {
validateDisplayName();
}
}, [ canProceed, displayName ] );

return (
<DataSourceFormStep
Expand Down Expand Up @@ -260,12 +299,20 @@ const DataSourceFormSetup = ( {
}
>
<InputControl
autoComplete="off"
// prevent 1password suggestions since they ignore autocomplete
data-1p-ignore
className={ `rdb-settings-page_data-source-form-input ${
errors.displayName ? 'has-error' : ''
} ` }
help={
<>
<span>
{ __( 'Only visible to you and other site managers. ', 'remote-data-blocks' ) }
{ errors.displayName
? errors.displayName
: __( 'Only visible to you and other site managers. ', 'remote-data-blocks' ) }
</span>
{ screen === 'editDataSource' && (
{ screen === 'editDataSource' && ! errors.displayName && (
<ModalWithButtonTrigger
buttonText={ __( 'Edit identifier', 'remote-data-blocks' ) }
isOpen={ isOpen }
Expand All @@ -281,33 +328,44 @@ const DataSourceFormSetup = ( {
if ( newUUID && isValidUUIDv4( newUUID ) ) {
isValidUUIDv4( newUUID );
handleOnChange( 'uuid', newUUID ?? '' );
setErrors( {} );
close();
} else {
setUUIDHelpText(
<span className="rdb-settings-page_data-source-form-uuid-error">
{ __(
'Please use valid UUIDv4 formatting to save changes.',
'remote-data-blocks'
) }
</span>
);
setErrors( {
uuid: __(
'Please use valid UUIDv4 formatting to save changes.',
'remote-data-blocks'
),
} );
}
} }
>
<InputControl
className={ `rdb-settings-page_data-source-form-input ${
errors.uuid ? 'has-error' : ''
} ` }
label={ __( 'UUID', 'remote-data-blocks' ) }
value={ newUUID ?? '' }
onChange={ ( uuid?: string ) => setNewUUID( uuid ?? null ) }
placeholcomponents-base-control__helpder={ uuidFromProps }
__next40pxDefaultSize
help={ UUIDHelpText }
help={
<span>
{ errors.uuid
? errors.uuid
: __(
'Unique identifier allows you to reference this data source in code.',
'remote-data-blocks'
) }
</span>
}
/>
<Spacer marginBottom={ 8 } />
<div className="rdb-settings-page_data-source-form-uuid-actions">
<Button
onClick={ () => {
setNewUUID( uuidFromProps ?? null );
setUUIDHelpText( defaultUUIDHelpText );
setErrors( {} );
close();
} }
variant="tertiary"
Expand All @@ -326,6 +384,7 @@ const DataSourceFormSetup = ( {
}
label={ __( 'Data Source Name' ) }
onChange={ onDisplayNameChange }
onBlur={ validateDisplayName }
value={ displayName }
prefix={
screen === 'editDataSource' ? (
Expand Down
3 changes: 3 additions & 0 deletions src/data-sources/components/PasswordInputControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const PasswordInputControl = ( { ...props }: PasswordInputControlProps ) => {

return (
<InputControl
autoComplete="off"
// prevent 1password suggestions since they ignore autocomplete
data-1p-ignore
className="password-input-control"
type={ visible ? 'text' : 'password' }
suffix={
Expand Down
Loading

0 comments on commit d106db9

Please sign in to comment.