@elementor/editor-components 3.33.0-99 → 3.34.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/index.js +1860 -123
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +1863 -110
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +21 -11
  6. package/src/api.ts +57 -11
  7. package/src/component-instance-transformer.ts +24 -0
  8. package/src/component-overridable-transformer.ts +28 -0
  9. package/src/components/components-tab/component-search.tsx +32 -0
  10. package/src/components/components-tab/components-item.tsx +67 -0
  11. package/src/components/components-tab/components-list.tsx +141 -0
  12. package/src/components/components-tab/components.tsx +17 -0
  13. package/src/components/components-tab/loading-components.tsx +43 -0
  14. package/src/components/components-tab/search-provider.tsx +38 -0
  15. package/src/components/consts.ts +1 -0
  16. package/src/components/create-component-form/create-component-form.tsx +109 -100
  17. package/src/components/create-component-form/utils/get-component-event-data.ts +54 -0
  18. package/src/components/create-component-form/utils/replace-element-with-component.ts +28 -10
  19. package/src/components/edit-component/component-modal.tsx +134 -0
  20. package/src/components/edit-component/edit-component.tsx +134 -0
  21. package/src/components/in-edit-mode.tsx +43 -0
  22. package/src/components/overridable-props/indicator.tsx +81 -0
  23. package/src/components/overridable-props/overridable-prop-form.tsx +98 -0
  24. package/src/components/overridable-props/overridable-prop-indicator.tsx +128 -0
  25. package/src/components/overridable-props/utils/get-overridable-prop.ts +20 -0
  26. package/src/create-component-type.ts +194 -0
  27. package/src/hooks/use-canvas-document.ts +6 -0
  28. package/src/hooks/use-components.ts +6 -9
  29. package/src/hooks/use-element-rect.ts +81 -0
  30. package/src/init.ts +82 -3
  31. package/src/mcp/index.ts +14 -0
  32. package/src/mcp/save-as-component-tool.ts +92 -0
  33. package/src/populate-store.ts +12 -0
  34. package/src/prop-types/component-overridable-prop-type.ts +17 -0
  35. package/src/store/actions.ts +21 -0
  36. package/src/store/components-styles-provider.ts +24 -0
  37. package/src/store/create-unpublished-component.ts +40 -0
  38. package/src/store/load-components-assets.ts +26 -0
  39. package/src/store/load-components-styles.ts +44 -0
  40. package/src/store/remove-component-styles.ts +9 -0
  41. package/src/store/set-overridable-prop.ts +161 -0
  42. package/src/store/store.ts +168 -0
  43. package/src/store/thunks.ts +10 -0
  44. package/src/sync/before-save.ts +15 -0
  45. package/src/sync/create-components-before-save.ts +108 -0
  46. package/src/sync/update-components-before-save.ts +36 -0
  47. package/src/types.ts +91 -0
  48. package/src/utils/component-document-data.ts +19 -0
  49. package/src/utils/get-component-ids.ts +36 -0
  50. package/src/utils/get-container-for-new-element.ts +49 -0
  51. package/src/utils/tracking.ts +47 -0
  52. package/src/components/components-tab.tsx +0 -6
  53. package/src/hooks/use-create-component.ts +0 -13
@@ -0,0 +1,54 @@
1
+ import { type V1ElementData } from '@elementor/editor-elements';
2
+
3
+ export type ComponentEventData = {
4
+ nested_elements_count: number;
5
+ nested_components_count: number;
6
+ top_element_type: string;
7
+ location?: string;
8
+ secondary_location?: string;
9
+ trigger?: string;
10
+ };
11
+
12
+ export type ContextMenuEventOptions = Record< string, unknown > & {
13
+ location: string;
14
+ secondaryLocation: string;
15
+ trigger: string;
16
+ };
17
+
18
+ export const getComponentEventData = (
19
+ containerElement: V1ElementData,
20
+ options?: ContextMenuEventOptions
21
+ ): ComponentEventData => {
22
+ const { elementsCount, componentsCount } = countNestedElements( containerElement );
23
+
24
+ return {
25
+ nested_elements_count: elementsCount,
26
+ nested_components_count: componentsCount,
27
+ top_element_type: containerElement.elType,
28
+ location: options?.location,
29
+ secondary_location: options?.secondaryLocation,
30
+ trigger: options?.trigger,
31
+ };
32
+ };
33
+
34
+ function countNestedElements( container: V1ElementData ): { elementsCount: number; componentsCount: number } {
35
+ if ( ! container.elements || container.elements.length === 0 ) {
36
+ return { elementsCount: 0, componentsCount: 0 };
37
+ }
38
+
39
+ let elementsCount = container.elements.length;
40
+ let componentsCount = 0;
41
+
42
+ for ( const element of container.elements ) {
43
+ if ( element.widgetType === 'e-component' ) {
44
+ componentsCount++;
45
+ }
46
+
47
+ const { elementsCount: nestedElementsCount, componentsCount: nestedComponentsCount } =
48
+ countNestedElements( element );
49
+ elementsCount += nestedElementsCount;
50
+ componentsCount += nestedComponentsCount;
51
+ }
52
+
53
+ return { elementsCount, componentsCount };
54
+ }
@@ -1,16 +1,34 @@
1
- import { replaceElement, type V1Element } from '@elementor/editor-elements';
2
- import { numberPropTypeUtil } from '@elementor/editor-props';
1
+ import { replaceElement, type V1ElementData, type V1ElementModelProps } from '@elementor/editor-elements';
3
2
 
4
- export const replaceElementWithComponent = async ( element: V1Element, componentId: number ) => {
3
+ type ComponentInstanceParams = {
4
+ id?: number;
5
+ name: string;
6
+ uid: string;
7
+ };
8
+
9
+ export const replaceElementWithComponent = ( element: V1ElementData, component: ComponentInstanceParams ) => {
5
10
  replaceElement( {
6
11
  currentElement: element,
7
- newElement: {
8
- elType: 'widget',
9
- widgetType: 'e-component',
10
- settings: {
11
- component_id: numberPropTypeUtil.create( componentId ),
12
- },
13
- },
12
+ newElement: createComponentModel( component ),
14
13
  withHistory: false,
15
14
  } );
16
15
  };
16
+
17
+ export const createComponentModel = ( component: ComponentInstanceParams ): Omit< V1ElementModelProps, 'id' > => {
18
+ return {
19
+ elType: 'widget',
20
+ widgetType: 'e-component',
21
+ settings: {
22
+ component_instance: {
23
+ $$type: 'component-instance',
24
+ value: {
25
+ component_id: component.id ?? component.uid,
26
+ },
27
+ },
28
+ },
29
+ editor_settings: {
30
+ title: component.name,
31
+ component_uid: component.uid,
32
+ },
33
+ };
34
+ };
@@ -0,0 +1,134 @@
1
+ import * as React from 'react';
2
+ import { type CSSProperties, useEffect } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ import { useCanvasDocument } from '../../hooks/use-canvas-document';
7
+ import { useElementRect } from '../../hooks/use-element-rect';
8
+
9
+ type ModalProps = {
10
+ element: HTMLElement;
11
+ onClose: () => void;
12
+ };
13
+ export function ComponentModal( { element, onClose }: ModalProps ) {
14
+ const canvasDocument = useCanvasDocument();
15
+
16
+ useEffect( () => {
17
+ const handleEsc = ( event: KeyboardEvent ) => {
18
+ if ( event.key === 'Escape' ) {
19
+ onClose();
20
+ }
21
+ };
22
+
23
+ canvasDocument?.body.addEventListener( 'keydown', handleEsc );
24
+
25
+ return () => {
26
+ canvasDocument?.body.removeEventListener( 'keydown', handleEsc );
27
+ };
28
+ }, [ canvasDocument, onClose ] );
29
+
30
+ if ( ! canvasDocument?.body ) {
31
+ return null;
32
+ }
33
+
34
+ return createPortal(
35
+ <>
36
+ <BlockEditPage />
37
+ <Backdrop canvas={ canvasDocument } element={ element } onClose={ onClose } />
38
+ </>,
39
+ canvasDocument.body
40
+ );
41
+ }
42
+
43
+ function Backdrop( { canvas, element, onClose }: { canvas: HTMLDocument; element: HTMLElement; onClose: () => void } ) {
44
+ const rect = useElementRect( element );
45
+ const backdropStyle: CSSProperties = {
46
+ position: 'fixed',
47
+ top: 0,
48
+ left: 0,
49
+ width: '100vw',
50
+ height: '100vh',
51
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
52
+ zIndex: 999,
53
+ pointerEvents: 'painted',
54
+ cursor: 'pointer',
55
+ clipPath: getRoundedRectPath( rect, canvas.defaultView as Window, 5 ),
56
+ };
57
+
58
+ const handleKeyDown = ( event: React.KeyboardEvent ) => {
59
+ if ( event.key === 'Enter' || event.key === ' ' ) {
60
+ event.preventDefault();
61
+ onClose();
62
+ }
63
+ };
64
+
65
+ return (
66
+ <div
67
+ style={ backdropStyle }
68
+ onClick={ onClose }
69
+ onKeyDown={ handleKeyDown }
70
+ role="button"
71
+ tabIndex={ 0 }
72
+ aria-label={ __( 'Exit component editing mode', 'elementor' ) }
73
+ />
74
+ );
75
+ }
76
+
77
+ function getRoundedRectPath( rect: DOMRect, viewport: Window, borderRadius: number ) {
78
+ const padding = borderRadius / 2;
79
+ const { x: originalX, y: originalY, width: originalWidth, height: originalHeight } = rect;
80
+ const x = originalX - padding;
81
+ const y = originalY - padding;
82
+ const width = originalWidth + 2 * padding;
83
+ const height = originalHeight + 2 * padding;
84
+ const radius = Math.min( borderRadius, width / 2, height / 2 );
85
+
86
+ const { innerWidth: vw, innerHeight: vh } = viewport;
87
+
88
+ const path = `path(evenodd, 'M 0 0
89
+ L ${ vw } 0
90
+ L ${ vw } ${ vh }
91
+ L 0 ${ vh }
92
+ Z
93
+ M ${ x + radius } ${ y }
94
+ L ${ x + width - radius } ${ y }
95
+ A ${ radius } ${ radius } 0 0 1 ${ x + width } ${ y + radius }
96
+ L ${ x + width } ${ y + height - radius }
97
+ A ${ radius } ${ radius } 0 0 1 ${ x + width - radius } ${ y + height }
98
+ L ${ x + radius } ${ y + height }
99
+ A ${ radius } ${ radius } 0 0 1 ${ x } ${ y + height - radius }
100
+ L ${ x } ${ y + radius }
101
+ A ${ radius } ${ radius } 0 0 1 ${ x + radius } ${ y }
102
+ Z'
103
+ )`;
104
+
105
+ return path.replace( /\s{2,}/g, ' ' );
106
+ }
107
+
108
+ /**
109
+ * when switching to another document id, we get a document handler when hovering
110
+ * this functionality originates in Pro, and is intended for editing templates, e.g. header/footer
111
+ * in components we don't want that, so the easy way out is to prevent it of being displayed via a CSS rule
112
+ */
113
+ function BlockEditPage() {
114
+ const blockV3DocumentHandlesStyles = `
115
+ .elementor-editor-active {
116
+ & .elementor-section-wrap.ui-sortable {
117
+ display: contents;
118
+ }
119
+
120
+ & *[data-editable-elementor-document]:not(.elementor-edit-mode):hover {
121
+ & .elementor-document-handle:not(.elementor-document-save-back-handle) {
122
+ display: none;
123
+
124
+ &::before,
125
+ & .elementor-document-handle__inner {
126
+ display: none;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ `;
132
+
133
+ return <style data-e-style-id="e-block-v3-document-handles-styles">{ blockV3DocumentHandlesStyles }</style>;
134
+ }
@@ -0,0 +1,134 @@
1
+ import * as React from 'react';
2
+ import { useCallback, useEffect } from 'react';
3
+ import { getV1DocumentsManager, type V1Document } from '@elementor/editor-documents';
4
+ import { type V1Element } from '@elementor/editor-elements';
5
+ import {
6
+ __privateListenTo as listenTo,
7
+ __privateRunCommand as runCommand,
8
+ commandEndEvent,
9
+ } from '@elementor/editor-v1-adapters';
10
+ import { __useSelector as useSelector } from '@elementor/store';
11
+
12
+ import { apiClient } from '../../api';
13
+ import { updateCurrentComponent } from '../../store/actions';
14
+ import { type ComponentsPathItem, selectCurrentComponentId, selectPath } from '../../store/store';
15
+ import { COMPONENT_DOCUMENT_TYPE } from '../consts';
16
+ import { ComponentModal } from './component-modal';
17
+
18
+ export function EditComponent() {
19
+ const { path, currentComponentId } = useCurrentComponent();
20
+
21
+ useHandleDocumentSwitches();
22
+
23
+ const onBack = useNavigateBack( path );
24
+
25
+ const elementDom = getComponentDOMElement( currentComponentId ?? undefined );
26
+
27
+ if ( ! elementDom ) {
28
+ return null;
29
+ }
30
+
31
+ return <ComponentModal element={ elementDom } onClose={ onBack } />;
32
+ }
33
+
34
+ function useHandleDocumentSwitches() {
35
+ const documentsManager = getV1DocumentsManager();
36
+ const { path, currentComponentId } = useCurrentComponent();
37
+
38
+ useEffect( () => {
39
+ return listenTo( commandEndEvent( 'editor/documents/attach-preview' ), () => {
40
+ const nextDocument = documentsManager.getCurrent();
41
+
42
+ if ( nextDocument.id === currentComponentId ) {
43
+ return;
44
+ }
45
+
46
+ if ( currentComponentId ) {
47
+ apiClient.unlockComponent( currentComponentId );
48
+ }
49
+
50
+ const isComponent = nextDocument.config.type === COMPONENT_DOCUMENT_TYPE;
51
+
52
+ if ( ! isComponent ) {
53
+ updateCurrentComponent( { path: [], currentComponentId: null } );
54
+ return;
55
+ }
56
+
57
+ updateCurrentComponent( {
58
+ path: getUpdatedComponentPath( path, nextDocument ),
59
+ currentComponentId: nextDocument.id,
60
+ } );
61
+ } );
62
+ }, [ path, documentsManager, currentComponentId ] );
63
+ }
64
+
65
+ function getUpdatedComponentPath( path: ComponentsPathItem[], nextDocument: V1Document ): ComponentsPathItem[] {
66
+ const componentIndex = path.findIndex( ( { componentId } ) => componentId === nextDocument.id );
67
+
68
+ if ( componentIndex >= 0 ) {
69
+ // When exiting the editing of a nested component - we in fact go back a step
70
+ // so we need to make sure the path is cleaned up of any newer items
71
+ // By doing it with the slice and not a simple pop() - we could jump to any component in the path and make sure it becomes the current one
72
+ return path.slice( 0, componentIndex + 1 );
73
+ }
74
+
75
+ return [
76
+ ...path,
77
+ {
78
+ instanceId: nextDocument?.container.view?.el?.dataset.id,
79
+ componentId: nextDocument.id,
80
+ },
81
+ ];
82
+ }
83
+
84
+ function useNavigateBack( path: ComponentsPathItem[] ) {
85
+ const documentsManager = getV1DocumentsManager();
86
+
87
+ return useCallback( () => {
88
+ const { componentId: prevComponentId, instanceId: prevComponentInstanceId } = path.at( -2 ) ?? {};
89
+
90
+ const switchToDocument = ( id: number, selector?: string ) => {
91
+ runCommand( 'editor/documents/switch', {
92
+ id,
93
+ selector,
94
+ mode: 'autosave',
95
+ setAsInitial: false,
96
+ shouldScroll: false,
97
+ } );
98
+ };
99
+
100
+ if ( prevComponentId && prevComponentInstanceId ) {
101
+ switchToDocument( prevComponentId, `[data-id="${ prevComponentInstanceId }"]` );
102
+
103
+ return;
104
+ }
105
+
106
+ switchToDocument( documentsManager.getInitialId() );
107
+ }, [ path, documentsManager ] );
108
+ }
109
+
110
+ function useCurrentComponent() {
111
+ const path = useSelector( selectPath );
112
+ const currentComponentId = useSelector( selectCurrentComponentId );
113
+
114
+ return {
115
+ path,
116
+ currentComponentId,
117
+ };
118
+ }
119
+
120
+ function getComponentDOMElement( id: V1Document[ 'id' ] | undefined ) {
121
+ if ( ! id ) {
122
+ return null;
123
+ }
124
+
125
+ const documentsManager = getV1DocumentsManager();
126
+
127
+ const currentComponent = documentsManager.get( id );
128
+
129
+ const widget = currentComponent?.container as V1Element;
130
+
131
+ const elementDom = widget?.children?.[ 0 ].view?.el;
132
+
133
+ return elementDom ?? null;
134
+ }
@@ -0,0 +1,43 @@
1
+ import * as React from 'react';
2
+ import { closeDialog, openDialog } from '@elementor/editor-ui';
3
+ import { InfoCircleFilledIcon } from '@elementor/icons';
4
+ import { Box, Button, DialogActions, DialogContent, DialogHeader, Icon, Stack, Typography } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ export const openEditModeDialog = ( lockedBy: string ) => {
8
+ openDialog( {
9
+ component: <EditModeDialog lockedBy={ lockedBy } />,
10
+ } );
11
+ };
12
+
13
+ const EditModeDialog = ( { lockedBy }: { lockedBy: string } ) => {
14
+ /* translators: %s is the name of the user who is currently editing the document */
15
+ const content = __( '%s is currently editing this document', 'elementor' ).replace( '%s', lockedBy );
16
+ return (
17
+ <>
18
+ <DialogHeader logo={ false }>
19
+ <Box display="flex" alignItems="center" gap={ 1 }>
20
+ <Icon color="secondary">
21
+ <InfoCircleFilledIcon fontSize="medium" />
22
+ </Icon>
23
+ <Typography variant="subtitle1">{ content }</Typography>
24
+ </Box>
25
+ </DialogHeader>
26
+ <DialogContent>
27
+ <Stack spacing={ 2 } direction="column">
28
+ <Typography variant="body2">
29
+ { __(
30
+ 'You can wait for them to finish or reach out to coordinate your changes together.',
31
+ 'elementor'
32
+ ) }
33
+ </Typography>
34
+ <DialogActions>
35
+ <Button color="secondary" variant="contained" onClick={ closeDialog }>
36
+ { __( 'Close', 'elementor' ) }
37
+ </Button>
38
+ </DialogActions>
39
+ </Stack>
40
+ </DialogContent>
41
+ </>
42
+ );
43
+ };
@@ -0,0 +1,81 @@
1
+ import * as React from 'react';
2
+ import { forwardRef } from 'react';
3
+ import { CheckIcon, PlusIcon } from '@elementor/icons';
4
+ import { type bindTrigger, Box, styled } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ const SIZE = 'tiny';
8
+
9
+ const IconContainer = styled( Box )`
10
+ pointer-events: none;
11
+ opacity: 0;
12
+ transition: opacity 0.2s ease-in-out;
13
+
14
+ & > svg {
15
+ position: absolute;
16
+ top: 50%;
17
+ left: 50%;
18
+ transform: translate( -50%, -50% );
19
+ width: 10px;
20
+ height: 10px;
21
+ fill: ${ ( { theme } ) => theme.palette.primary.contrastText };
22
+ stroke: ${ ( { theme } ) => theme.palette.primary.contrastText };
23
+ stroke-width: 2px;
24
+ }
25
+ `;
26
+
27
+ const Content = styled( Box )`
28
+ position: relative;
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ cursor: pointer;
33
+ width: 16px;
34
+ height: 16px;
35
+ margin-inline: ${ ( { theme } ) => theme.spacing( 0.5 ) };
36
+
37
+ &:before {
38
+ content: '';
39
+ display: block;
40
+ position: absolute;
41
+ top: 50%;
42
+ left: 50%;
43
+ transform: translate( -50%, -50% ) rotate( 45deg );
44
+ width: 5px;
45
+ height: 5px;
46
+ border-radius: 1px;
47
+ background-color: ${ ( { theme } ) => theme.palette.primary.main };
48
+ transition: all 0.1s ease-in-out;
49
+ }
50
+
51
+ &:hover,
52
+ &.enlarged {
53
+ &:before {
54
+ width: 12px;
55
+ height: 12px;
56
+ border-radius: 2px;
57
+ }
58
+
59
+ .icon {
60
+ opacity: 1;
61
+ }
62
+ }
63
+ `;
64
+
65
+ type Props = {
66
+ isOverridable: boolean;
67
+ triggerProps: ReturnType< typeof bindTrigger >;
68
+ isOpen: boolean;
69
+ };
70
+ export const Indicator = forwardRef< HTMLDivElement, Props >( ( { triggerProps, isOpen, isOverridable }, ref ) => (
71
+ <Content ref={ ref } { ...triggerProps } className={ isOpen || isOverridable ? 'enlarged' : '' }>
72
+ <IconContainer
73
+ className="icon"
74
+ aria-label={
75
+ isOverridable ? __( 'Overridable property', 'elementor' ) : __( 'Make prop overridable', 'elementor' )
76
+ }
77
+ >
78
+ { isOverridable ? <CheckIcon fontSize={ SIZE } /> : <PlusIcon fontSize={ SIZE } /> }
79
+ </IconContainer>
80
+ </Content>
81
+ ) );
@@ -0,0 +1,98 @@
1
+ import * as React from 'react';
2
+ import { useState } from 'react';
3
+ import { Form, MenuListItem } from '@elementor/editor-ui';
4
+ import { Button, FormLabel, Grid, Select, Stack, TextField, Typography } from '@elementor/ui';
5
+ import { __ } from '@wordpress/i18n';
6
+
7
+ import { type OverridableProp } from '../../types';
8
+
9
+ const SIZE = 'tiny';
10
+
11
+ const DEFAULT_GROUP = { value: null, label: __( 'Default', 'elementor' ) };
12
+
13
+ type Props = {
14
+ onSubmit: ( data: { label: string; group: string | null } ) => void;
15
+ currentValue?: OverridableProp;
16
+ groups?: { value: string; label: string }[];
17
+ };
18
+
19
+ export function OverridablePropForm( { onSubmit, groups, currentValue }: Props ) {
20
+ const [ propLabel, setPropLabel ] = useState< string | null >( currentValue?.label ?? null );
21
+ const [ group, setGroup ] = useState< string | null >( currentValue?.groupId ?? groups?.[ 0 ]?.value ?? null );
22
+
23
+ const name = __( 'Name', 'elementor' );
24
+ const groupName = __( 'Group Name', 'elementor' );
25
+
26
+ const isCreate = currentValue === undefined;
27
+
28
+ const title = isCreate ? __( 'Create new property', 'elementor' ) : __( 'Update property', 'elementor' );
29
+ const ctaLabel = isCreate ? __( 'Create', 'elementor' ) : __( 'Update', 'elementor' );
30
+
31
+ return (
32
+ <Form onSubmit={ () => onSubmit( { label: propLabel ?? '', group } ) }>
33
+ <Stack alignItems="start" width="268px">
34
+ <Stack
35
+ direction="row"
36
+ alignItems="center"
37
+ py={ 1 }
38
+ px={ 1.5 }
39
+ sx={ { columnGap: 0.5, borderBottom: '1px solid', borderColor: 'divider', width: '100%', mb: 1.5 } }
40
+ >
41
+ <Typography variant="caption" sx={ { color: 'text.primary', fontWeight: '500', lineHeight: 1 } }>
42
+ { title }
43
+ </Typography>
44
+ </Stack>
45
+ <Grid container gap={ 0.75 } alignItems="start" px={ 1.5 } mb={ 1.5 }>
46
+ <Grid item xs={ 12 }>
47
+ <FormLabel size="tiny">{ name }</FormLabel>
48
+ </Grid>
49
+ <Grid item xs={ 12 }>
50
+ <TextField
51
+ name={ name }
52
+ size={ SIZE }
53
+ fullWidth
54
+ placeholder={ __( 'Enter value', 'elementor' ) }
55
+ value={ propLabel ?? '' }
56
+ onChange={ ( e: React.ChangeEvent< HTMLInputElement > ) => setPropLabel( e.target.value ) }
57
+ />
58
+ </Grid>
59
+ </Grid>
60
+ <Grid container gap={ 0.75 } alignItems="start" px={ 1.5 } mb={ 1.5 }>
61
+ <Grid item xs={ 12 }>
62
+ <FormLabel size="tiny">{ groupName }</FormLabel>
63
+ </Grid>
64
+ <Grid item xs={ 12 }>
65
+ <Select
66
+ name={ groupName }
67
+ size={ SIZE }
68
+ fullWidth
69
+ value={ group ?? null }
70
+ onChange={ setGroup }
71
+ displayEmpty
72
+ renderValue={ ( selectedValue: string | null ) => {
73
+ if ( ! selectedValue || selectedValue === '' ) {
74
+ const [ firstGroup = DEFAULT_GROUP ] = groups ?? [];
75
+
76
+ return firstGroup.label;
77
+ }
78
+
79
+ return groups?.find( ( { value } ) => value === selectedValue )?.label ?? selectedValue;
80
+ } }
81
+ >
82
+ { ( groups ?? [ DEFAULT_GROUP ] ).map( ( { label: groupLabel, ...props } ) => (
83
+ <MenuListItem key={ props.value } { ...props } value={ props.value ?? '' }>
84
+ { groupLabel }
85
+ </MenuListItem>
86
+ ) ) }
87
+ </Select>
88
+ </Grid>
89
+ </Grid>
90
+ <Stack direction="row" justifyContent="flex-end" alignSelf="end" mt={ 1.5 } py={ 1 } px={ 1.5 }>
91
+ <Button type="submit" disabled={ ! propLabel } variant="contained" color="primary" size="small">
92
+ { ctaLabel }
93
+ </Button>
94
+ </Stack>
95
+ </Stack>
96
+ </Form>
97
+ );
98
+ }