[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/mutations/SaveContent.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { Mutation, MutationFn, MutationResult } from 'react-apollo'
import SaveContent from '../graphql/SaveContent.graphql'

interface SaveContentData {
  saveContent: Pick<ExtensionConfiguration, 'contentId'>
}

interface SaveContentVariables {
  bindingId?: string
  blockId?: string
  configuration: Omit<ExtensionConfiguration, 'contentId'> & {
    contentId: string | null
  }
  lang: RenderContext['culture']['locale']
  template: string
  treePath: string
}

export type SaveContentMutationFn = MutationFn<
  SaveContentData,
  SaveContentVariables
>

export interface MutationRenderProps extends MutationResult<SaveContentData> {
  SaveContent: SaveContentMutationFn
}

class SaveContentMutation extends Mutation<
  SaveContentData,
  SaveContentVariables
> {
  public static defaultProps = {
    mutation: SaveContent,
  }
}

export default SaveContentMutation

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/queries/ListContent.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { Query, QueryResult } from 'react-apollo'
import ListContent from '../graphql/ListContent.graphql'

export const ListContentGraphqlDocument = ListContent

export interface ListContentVariables {
  bindingId?: string
  blockId: string
  pageContext: { id: string; type: string }
  template: string
  treePath: string
}

export interface ListContentData {
  listContentWithSchema?: {
    content?: ExtensionConfiguration[]
    schemaJSON: string
  }
}

export type ListContentQueryResult = QueryResult<
  ListContentData,
  ListContentVariables
>

class ListContentQuery extends Query<ListContentData, ListContentVariables> {
  public static defaultProps = {
    fetchPolicy: 'network-only',
    query: ListContent,
  }
}

export default ListContentQuery

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Styles/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { useEffect, useState } from 'react'

import { useEditorContext } from '../../EditorContext'

import StyleEditor from './StyleEditor'
import StyleList from './StyleList'

const SELECTED_STYLE_TAG_ID = 'style_link'
const PATH_STYLE_TAG_ID = 'style_path'
const SHEET_STYLE_TAG_ID = 'style_sheet'

type EditingState = Style | undefined

const setStyleAsset = (window?: Window) => (asset: StyleAssetInfo) => {
  if (!window) {
    return
  }

  if (asset.type === 'path') {
    const pathStyleTag = window.document.getElementById(PATH_STYLE_TAG_ID)
    if (pathStyleTag) {
      pathStyleTag.setAttribute('href', asset.value)
    }
    if (!asset.keepSheet) {
      const sheetStyleTag = window.document.getElementById(SHEET_STYLE_TAG_ID)
      if (sheetStyleTag) {
        sheetStyleTag.innerHTML = ''
      }
    }
    if (asset.selected) {
      const selectedStyleTag = window.document.getElementById(
        SELECTED_STYLE_TAG_ID
      )
      if (selectedStyleTag) {
        selectedStyleTag.setAttribute('href', asset.value)
      }
    }
  } else {
    const sheetStyleTag = window.document.getElementById(SHEET_STYLE_TAG_ID)
    if (sheetStyleTag) {
      sheetStyleTag.innerHTML = asset.value
    }
  }
}

const createStyleTag = (id: string, window?: Window) => {
  if (!window || window.document.getElementById(id)) {
    return
  }
  const styleTag = window.document.createElement('style')
  styleTag.setAttribute('id', id)
  const tachyonsTag = window.document.getElementById('style_link')
  if (tachyonsTag) {
    tachyonsTag.after(styleTag)
  }
}

const removeStyleTag = (id: string, window?: Window) => {
  if (!window) {
    return
  }

  const styleTag = window.document.getElementById(id)
  if (styleTag && window.document.head) {
    try {
      window.document.head.removeChild(styleTag)
    } catch (err) {
      console.error(err)
    }
  }
}

const Styles = () => {
  const [editing, setEditing] = useState<EditingState>(undefined)

  const { iframeWindow } = useEditorContext()

  useEffect(() => {
    createStyleTag(PATH_STYLE_TAG_ID, iframeWindow)
    createStyleTag(SHEET_STYLE_TAG_ID, iframeWindow)

    return () => {
      removeStyleTag(PATH_STYLE_TAG_ID, iframeWindow)
      removeStyleTag(SHEET_STYLE_TAG_ID, iframeWindow)
    }
  }, [editing, iframeWindow])

  return (
    <div
      className="h-100 mr5 bg-base ba b--muted-4 br3"
      style={{
        minWidth: '31rem',
        width: '31rem',
      }}
    >
      {editing ? (
        <StyleEditor
          style={editing}
          stopEditing={() => setEditing(undefined)}
          setStyleAsset={setStyleAsset(iframeWindow)}
        />
      ) : (
        <StyleList
          startEditing={setEditing}
          setStyleAsset={setStyleAsset(iframeWindow)}
        />
      )}
    </div>
  )
}

export default Styles

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/graphql/SaveContent.graphql from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```graphql
mutation saveContent(
  $bindingId: String
  $blockId: String
  $configuration: ContentConfigurationInput
  $template: String
  $treePath: String
  $lang: String
) {
  saveContent(
    bindingId: $bindingId
    blockId: $blockId
    configuration: $configuration
    template: $template
    treePath: $treePath
    lang: $lang
  ) {
    # Available fields:
    # condition {
    #   allMatches
    #   id
    #   pageContext {
    #     id
    #     type
    #   }
    #   statements {
    #     objectJSON
    #     subject
    #     verb
    #   }
    # }
    # contentJSON
    # contentId
    # label
    contentId
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/graphql/SendEventToAudit.graphql from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```graphql
mutation SendEventToAudit($input: SendToAuditInput!) {
  sendEventToAudit(input: $input) {
    id
    mainAccountName
    date
    application
    accountName
    subjectId
    workspace
    operation
    meta {
      entityName
      entityBeforeAction
      entityAfterAction
      remoteIpAddress
      forwardFromVtexUserAgent
    }
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/graphql/DeleteContent.graphql from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```graphql
mutation DeleteContent(
  $template: String
  $treePath: String
  $pageContext: PageContextInput
  $contentId: String
) {
  deleteContent(
    template: $template
    treePath: $treePath
    pageContext: $pageContext
    contentId: $contentId
  )
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/graphql/ListContent.graphql from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```graphql
query ListContent(
  $bindingId: String
  $blockId: String
  $pageContext: PageContextInput
  $template: String
  $treePath: String
) {
  listContentWithSchema(
    bindingId: $bindingId
    blockId: $blockId
    pageContext: $pageContext
    template: $template
    treePath: $treePath
  ) {
    content {
      condition {
        allMatches
        id
        pageContext {
          id
          type
        }
        statements {
          objectJSON
          subject
          verb
        }
      }
      contentId
      contentJSON
      label
      origin
    }
    schemaJSON
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/EditableText/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { IconEdit } from 'vtex.styleguide'

import styles from './styles.css'

interface Props {
  baseClassName: string
  onChange?: React.ChangeEventHandler<HTMLInputElement>
  onBlur?: React.FocusEventHandler<HTMLInputElement>
  onKeyPress?: React.KeyboardEventHandler<HTMLInputElement>
  disabled?: boolean
  placeholder?: string
  value?: string
}

const EditableText: React.FC<Props> = ({
  baseClassName,
  onChange,
  onBlur,
  onKeyPress,
  disabled,
  placeholder,
  value,
}) => {
  const [isEditing, setIsEditing] = React.useState(false)

  const inputRef = React.useRef<HTMLInputElement | null>(null)

  const startEditing = React.useCallback(() => {
    inputRef && inputRef.current && inputRef.current.focus()
  }, [inputRef])

  const handleOnFocus = React.useCallback(() => {
    setIsEditing(true)
  }, [setIsEditing])

  const handleOnBlur = React.useCallback(
    e => {
      setIsEditing(false)
      onBlur && onBlur(e)
    },
    [setIsEditing, onBlur]
  )

  return (
    <div
      role="presentation"
      className={`flex items-baseline bb b--light-gray ${
        isEditing ? 'hover-b--action-primary' : ''
      } pb2 ${isEditing ? 'b--action-primary' : ''} ${
        styles.editableTextInputWrapper
      }`}
      style={{ cursor: 'text' }}
      onClick={startEditing}
    >
      <input
        className={`flex-grow-1 w-100 truncate outline-0 c-on-base bn font-body input-reset ${baseClassName} ${styles.input}`}
        onChange={onChange}
        onFocus={handleOnFocus}
        onBlur={handleOnBlur}
        disabled={disabled}
        onKeyPress={onKeyPress}
        placeholder={placeholder || ''}
        ref={inputRef}
        type="text"
        value={value || ''}
      />
      <span
        className={`${styles.editableTextInputWrapper__input} ${
          isEditing ? 'dn' : ''
        }`}
      >
        <IconEdit />
      </span>
    </div>
  )
}

export default EditableText

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/EditableText/styles.css from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```css
.input::placeholder {
  color: #979899;
  font-style: normal;
  font-weight: 400;
}

.editableTextInputWrapper .editableTextInputWrapper__input {
  opacity: 0;
}

.editableTextInputWrapper:hover .editableTextInputWrapper__input {
  opacity: 1;
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Topbar/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { ToastConsumer } from 'vtex.styleguide'

import BlockPicker from './BlockPicker'
import ContextSelectors from './ContextSelectors'
import DeviceSwitcher from './DeviceSwitcher'
import SidebarVisibilityToggle from './SidebarVisibilityToggle'
import UrlInput from './UrlInput'

import styles from '../EditorContainer.css'

interface Props {
  iframeRuntime: RenderContext
}

const Topbar: React.FunctionComponent<Props> = ({ iframeRuntime }) => (
  <div
    className={`ph5 pv7 f6 ${styles['h-3em']} w-100 bg-muted-5 flex justify-center items-center`}
  >
    <ToastConsumer>
      {({ showToast }) => (
        <ContextSelectors iframeRuntime={iframeRuntime} showToast={showToast} />
      )}
    </ToastConsumer>

    <div className="ml6 flex flex-grow-1">
      <UrlInput />
    </div>

    <div className="ml6">
      <BlockPicker />
    </div>

    <div className="ml8">
      <DeviceSwitcher device={iframeRuntime.device} />
    </div>

    <div className="ml6">
      <SidebarVisibilityToggle />
    </div>
  </div>
)

export default Topbar

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Topbar/BlockPicker.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { useKeydownFromClick } from 'keydown-from-click'
import React from 'react'
import { InjectedIntlProps, injectIntl } from 'react-intl'
import { Tooltip } from 'vtex.styleguide'

import { useEditorContext } from '../../EditorContext'
import { useHover } from './hooks'
import IconPicker from './icons/IconPicker'

const BlockPicker: React.FC<InjectedIntlProps> = ({ intl }) => {
  const editor = useEditorContext()

  const { handleMouseEnter, handleMouseLeave, hover } = useHover()

  const handleEditModeToggle = editor.toggleEditMode

  const handleKeyPress = useKeydownFromClick(handleEditModeToggle)

  return (
    <Tooltip
      label={intl.formatMessage({
        defaultMessage: 'Select a block',
        id: 'admin/pages.editor.topbar.button.block-picker.tooltip',
      })}
      position="bottom"
    >
      <button
        style={editor.mode === 'disabled' ? { cursor: 'wait' } : {}}
        className={`w2 h2 bg-white br2 b--transparent outline-0 pointer flex justify-center items-center ${
          editor.editMode || (hover && editor.mode !== 'disabled')
            ? 'c-action-primary'
            : 'c-on-disabled'
        }`}
        onClick={handleEditModeToggle}
        onKeyPress={handleKeyPress}
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
        disabled={editor.mode === 'disabled'}
      >
        <IconPicker />
      </button>
    </Tooltip>
  )
}

export default injectIntl(BlockPicker)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Topbar/SidebarVisibilityToggle.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { createKeydownFromClick } from 'keydown-from-click'
import React from 'react'
import { InjectedIntlProps, injectIntl } from 'react-intl'
import { Tooltip } from 'vtex.styleguide'

import { useEditorContext } from '../../EditorContext'

import { useHover } from './hooks'
import IconView from './icons/IconView'

const SidebarVisibilityToggle: React.FC<InjectedIntlProps> = ({ intl }) => {
  const editor = useEditorContext()

  const { handleMouseEnter, handleMouseLeave, hover } = useHover()

  const handleSidebarVisibilityToggle = editor.toggleSidebarVisibility

  const handleSidebarVisibilityToggleKeyDown = createKeydownFromClick(
    handleSidebarVisibilityToggle
  )

  return (
    <Tooltip
      label={intl.formatMessage({
        defaultMessage: 'Preview',
        id: 'admin/pages.editor.topbar.button.preview.tooltip',
      })}
      position="bottom"
    >
      <button
        className={`w2 h2 bg-white br2 b--transparent outline-0 pointer flex justify-center items-center ${
          editor.isSidebarVisible && !hover
            ? 'c-on-disabled'
            : 'c-action-primary'
        }`}
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
        onClick={handleSidebarVisibilityToggle}
        onKeyDown={handleSidebarVisibilityToggleKeyDown}
      >
        <IconView />
      </button>
    </Tooltip>
  )
}

export default injectIntl(SidebarVisibilityToggle)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Topbar/UrlInput.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { FormattedMessage } from 'react-intl'

import { useEditorContext } from '../../EditorContext'

const UrlInput = () => {
  const [urlInputDisabled, setUrlInputDisabled] = React.useState(false)

  const editor = useEditorContext()

  const resolveUrlPath = (pathname: string, searchQueries: string) => {
    const searchParams = new URLSearchParams(searchQueries)
    const SEARCH_QUERIES_TO_HIDE = [
      '__siteEditor',
      '__bindingAddress',
      '__locale',
    ]

    SEARCH_QUERIES_TO_HIDE.forEach(query => {
      searchParams.delete(query)
    })

    if (searchParams.toString().length) {
      return pathname + decodeURIComponent(`?${searchParams.toString()}`)
    }

    return pathname
  }

  const urlPath = editor.iframeWindow
    ? resolveUrlPath(
        editor.iframeWindow.location.pathname,
        editor.iframeWindow.location.search
      )
    : ''

  const [url, setUrl] = React.useState(urlPath)

  React.useEffect(() => {
    setUrl(urlPath)
  }, [urlPath])

  const onEnter = (
    event: React.KeyboardEvent<HTMLInputElement>,
    callback: () => void
  ) => event.key === 'Enter' && callback()

  const handleNavigateToUrl = () => {
    if (url === urlPath) {
      return
    }

    setUrlInputDisabled(true)

    editor.onChangeIframeUrl(url)
  }

  const handleUrlKeyPress = (
    e: React.KeyboardEvent<HTMLInputElement>
  ): void => {
    onEnter(e, handleNavigateToUrl)
  }

  const handleChangeUrl = (e: React.ChangeEvent<HTMLInputElement>) => {
    const targetUrl = e.target.value
    if (targetUrl === url) {
      return
    }
    setUrl(targetUrl)
  }

  return (
    <div className="w-100 h2 pl5 bg-white ba bw1 br2 b--muted-4 hover-b--muted-3 flex items-center">
      <FormattedMessage
        defaultMessage="URL"
        id="admin/pages.editor.container.editpath.label"
      >
        {message => (
          <label
            className="c-muted-2"
            htmlFor="topbar-url-input"
            style={{ userSelect: 'none' }}
          >
            {message}
          </label>
        )}
      </FormattedMessage>

      <input
        className="w-100 pl3 bn outline-0"
        disabled={urlInputDisabled}
        id="topbar-url-input"
        onBlur={handleNavigateToUrl}
        onChange={handleChangeUrl}
        onKeyPress={handleUrlKeyPress}
        value={url}
      />
    </div>
  )
}

export default UrlInput

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Topbar/hooks.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { useCallback, useState } from 'react'

export const useHover = () => {
  const [hover, setHover] = useState(false)

  const handleMouseEnter = useCallback(() => {
    setHover(true)
  }, [])

  const handleMouseLeave = useCallback(() => {
    setHover(false)
  }, [])

  return { handleMouseEnter, handleMouseLeave, hover }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/BlockEditor/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { injectIntl } from 'react-intl'
import { CSSTransition } from 'react-transition-group'

import { ANIMATION_TIMEOUT } from '../../../consts'
import { useEditorContext } from '../../../EditorContext'
import DeleteContentMutation from '../../mutations/DeleteContent'
import Transitions from '../Transitions'
import { EditingState, FormDataContainer } from '../typings'
import BlockConfigurationEditor from './BlockConfigurationEditor'
import BlockConfigurationList from './BlockConfigurationList'
import { useFormHandlers } from './hooks'
import { UseFormHandlersParams } from './typings'
import { getActiveContentId } from '../../../../utils/components/index'

type Props = Omit<UseFormHandlersParams, 'setState' | 'state'> & {
  initialEditingState?: EditingState
}

export enum ConfigurationStatus {
  ACTIVE = 'ACTIVE',
  INACTIVE = 'INACTIVE',
  SCHEDULED = 'SCHEDULED',
}

export interface State extends EditingState {
  mode: 'editingActive' | 'editingInactive' | 'list'
  prevMode?: State['mode']
  status?: ConfigurationStatus
}

const BlockEditor = ({
  iframeRuntime,
  initialEditingState,
  intl,
  saveContent,
  sendEventToAudit,
  showToast,
}: Props) => {
  const [state, setState] = React.useReducer<
    React.Reducer<State, Partial<State>>
  >(
    (prevState, nextState) => ({
      ...prevState,
      ...nextState,
      prevMode:
        nextState.mode && nextState.mode !== prevState.mode
          ? prevState.mode
          : prevState.prevMode,
    }),
    { ...initialEditingState, mode: 'editingActive' }
  )

  const isFirstState = !state.prevMode

  const stateTransitions = React.useMemo(
    () => ({
      editingActiveToList:
        state.prevMode === 'editingActive' && state.mode === 'list',
      editingToList:
        state.prevMode &&
        state.prevMode.startsWith('editing') &&
        state.mode === 'list',
      listToEditingActive:
        state.prevMode === 'list' && state.mode === 'editingActive',
      listToEditing:
        state.prevMode === 'list' && state.mode.startsWith('editing'),
    }),
    [state.mode, state.prevMode]
  )

  const editor = useEditorContext()

  const activeContentId = getActiveContentId({
    extensions: iframeRuntime.extensions,
    treePath: editor.editTreePath,
  })

  const currentContent = editor.blockData?.configurations?.find(
    config => state.contentId === config.contentId
  )

  const statusFromRuntime =
    currentContent?.contentId === activeContentId
      ? ConfigurationStatus.ACTIVE
      : ConfigurationStatus.INACTIVE

  const {
    handleStatusChange,
    handleConditionChange,
    handleConfigurationCreate,
    handleConfigurationOpen,
    handleConfigurationActivate,
    handleFormBack,
    handleFormChange,
    handleFormSave,
    handleLabelChange,
    handleListClose,
    handleListOpen,
  } = useFormHandlers({
    iframeRuntime,
    intl,
    saveContent,
    sendEventToAudit,
    setState,
    showToast,
    state,
    statusFromRuntime,
  })

  return (
    <>
      <CSSTransition
        in={isFirstState || stateTransitions.listToEditing}
        mountOnEnter
        timeout={ANIMATION_TIMEOUT}
        unmountOnExit
      >
        <BlockConfigurationEditor
          state={state}
          statusFromRuntime={statusFromRuntime}
          condition={
            (state
              ? state.condition
              : {}) as ExtensionConfiguration['condition']
          }
          isNew={!currentContent}
          contentSchema={editor.blockData.contentSchema}
          editingContentId={state.contentId}
          data={state.formData as FormDataContainer}
          iframeRuntime={iframeRuntime}
          isSitewide={editor.blockData.isSitewide}
          label={state.label}
          onBack={handleFormBack}
          onChange={handleFormChange}
          onConditionChange={handleConditionChange}
          onStatusChange={handleStatusChange}
          onLabelChange={handleLabelChange}
          onListOpen={handleListOpen}
          onSave={handleFormSave}
          showToast={showToast}
          title={editor.blockData.title}
        />
      </CSSTransition>

      <Transitions.Exit condition={stateTransitions.listToEditing} to="right">
        <Transitions.Enter
          condition={stateTransitions.editingToList}
          from="right"
        >
          <DeleteContentMutation>
            {deleteContent => (
              <BlockConfigurationList
                deleteContent={deleteContent}
                sendEventToAudit={sendEventToAudit}
                onConfigurationActivate={handleConfigurationActivate}
                editingContentId={state.contentId}
                iframeRuntime={iframeRuntime}
                onConfigurationCreate={handleConfigurationCreate}
                onConfigurationOpen={handleConfigurationOpen}
                onListClose={handleListClose}
                showToast={showToast}
              />
            )}
          </DeleteContentMutation>
        </Transitions.Enter>
      </Transitions.Exit>
    </>
  )
}

export default injectIntl(BlockEditor)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/BlockEditor/EditorHeader.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { useKeydownFromClick } from 'keydown-from-click'
import React from 'react'
import { injectIntl, InjectedIntlProps } from 'react-intl'
import { IconArrowBack, IconClose } from 'vtex.styleguide'

interface CustomProps {
  isTitleEditable?: boolean
  onBack?: () => void
  onListClose?: () => void
  onListOpen?: () => void
  onTitleChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
  title?: string
}

type Props = CustomProps & InjectedIntlProps

const noOp = () => {
  return
}

const EditorHeader: React.FC<Props> = ({
  intl,
  onBack,
  onListClose,
  onListOpen,
  title,
}) => {
  const handleBackKeyDown = useKeydownFromClick(onBack || noOp)
  const handleCloseListKeyDown = useKeydownFromClick(onListClose || noOp)
  const handleOpenListKeyDown = useKeydownFromClick(onListOpen || noOp)

  return (
    <div className="ph5 pt4">
      <div className="w-100 flex justify-between">
        <div
          className="flex items-center c-action-primary hover-c-action-primary pointer"
          role="button"
          tabIndex={0}
          onKeyDown={handleBackKeyDown}
          onClick={onBack}
        >
          {onBack && (
            <React.Fragment>
              <span className="outline-0">
                <IconArrowBack />
              </span>
              <span className="ml3 fw5 f7 ttu">
                {intl.formatMessage({
                  defaultMessage: 'Back',
                  id: 'admin/pages.editor.component-list.back',
                })}
              </span>
            </React.Fragment>
          )}
        </div>
        <div>
          {onListClose && (
            <span
              className="pointer outline-0 c-on-base hover-c-action-primary"
              onClick={onListClose}
              onKeyDown={handleCloseListKeyDown}
              role="button"
              tabIndex={0}
            >
              <IconClose />
            </span>
          )}

          {onListOpen && (
            <span
              className="pointer outline-0 c-action-primary hover-c-action-primary ttu f7 fw5"
              role="button"
              tabIndex={0}
              onClick={onListOpen}
              onKeyDown={handleOpenListKeyDown}
            >
              {intl.formatMessage({
                defaultMessage: 'Configurations',
                id: 'admin/pages.editor.component-list.title',
              })}
            </span>
          )}
        </div>
      </div>

      <div className="w-100 flex flex-grow-1 justify-between items-center">
        <h4 className="w-100 ma0 lh-copy f4 fw4 near-black ba b--transparent">
          {title}
        </h4>
      </div>
    </div>
  )
}

export default injectIntl(EditorHeader)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/BlockEditor/utils.test.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { getIsDefaultContent } from './utils'

describe('getIsDefaultContent', () => {
  it('should return true when origin is declared (comes from app)', () => {
    expect(getIsDefaultContent({ origin: 'comes from block' })).toBe(true)
  })

  it('should return false when origin is null (declared by user)', () => {
    expect(getIsDefaultContent({ origin: null })).toBe(false)
  })
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/BlockEditor/utils.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import throttle from 'lodash/throttle'

import { updateExtensionFromForm } from '../../../../utils/components'
import { NEW_CONFIGURATION_ID } from '../consts'
import { getDefaultCondition } from '../utils'
import { GetDefaultConfiguration, GetConfigurationType } from './typings'

export const getDefaultConfiguration: GetDefaultConfiguration = ({
  iframeRuntime,
  isSitewide,
}) => ({
  condition: getDefaultCondition({ iframeRuntime, isSitewide }),
  contentId: NEW_CONFIGURATION_ID,
  contentJSON: '{}',
  label: null,
  origin: null,
})

export const getIsDefaultContent: (
  configuration: Pick<ExtensionConfiguration, 'origin'>
) => boolean = configuration => configuration.origin !== null

export const omitUndefined = (obj: Extension['content']) =>
  Object.entries(obj).reduce((acc, [currKey, currValue]) => {
    if (typeof currValue === 'undefined') {
      return acc
    }

    return { ...acc, [currKey]: currValue }
  }, {})

export const getConfigurationType: GetConfigurationType = ({
  configuration,
  activeContentId,
}) => {
  if (getIsDefaultContent(configuration)) {
    return 'app'
  }

  if (activeContentId === configuration.contentId) {
    return 'active'
  }

  return 'inactive'
}

export const throttledUpdateExtensionFromForm = throttle(
  data => updateExtensionFromForm(data),
  200
)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/BlockEditor/typings.d.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { MutationFn } from 'react-apollo'
import { InjectedIntl } from 'react-intl'
import { FormProps } from 'react-jsonschema-form'
import { ToastConsumerFunctions } from 'vtex.styleguide'

import { GetDefaultConditionParams } from '../typings'
import { ConfigurationStatus, State as FormState } from './index'
import { SendEventToAuditMutationFn } from '../../mutations/SendEventToAudit'

export type GetDefaultConfiguration = (
  params: GetDefaultConditionParams
) => ExtensionConfiguration

export interface GetDefaultConditionParams {
  iframeRuntime: RenderContext
  isSitewide: boolean
}

interface GetConfigurationTypeParams {
  configuration: ExtensionConfiguration
  activeContentId: ExtensionConfiguration['contentId']
}

type ConfigurationType = 'active' | 'inactive' | 'app'

type GetConfigurationType = (
  params: GetConfigurationTypeParams
) => ConfigurationType

export interface UseFormHandlersParams {
  iframeRuntime: RenderContext
  intl: InjectedIntl
  saveContent: MutationFn<SaveContentData, SaveContentVariables>
  sendEventToAudit: SendEventToAuditMutationFn
  setState: React.Dispatch<Partial<FormState>>
  showToast: ToastConsumerFunctions['showToast']
  state: FormState
  statusFromRuntime?: ConfigurationStatus
}

export type UseFormHandlers = (
  params: UseFormHandlersParams
) => {
  handleStatusChange: () => void
  handleConfigurationActivate: (
    configuration: ExtensionConfiguration
  ) => Promise<void>
  handleConditionChange: (changes: Partial<FormState['condition']>) => void
  handleConfigurationCreate: () => ReturnType<
    ReturnType<UseFormHandlers>['handleInactiveConfigurationOpen']
  >
  handleConfigurationOpen: (
    configuration: ExtensionConfiguration
  ) => Promise<void>
  handleFormBack: () => void
  handleFormChange: FormProps<FormDataContainer>['onChange']
  handleFormSave: () => Promise<void>
  handleLabelChange: React.ChangeEventHandler<HTMLInputElement>
  handleListClose: () => void
  handleListOpen: () => void
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/BlockEditor/hooks.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { equals, path } from 'ramda'
import { useCallback } from 'react'
import { defineMessages } from 'react-intl'
import { useRuntime } from 'vtex.render-runtime'

import { ConfigurationStatus } from '.'
import {
  getActiveContentId,
  getSchemaPropsOrContent,
  updateExtensionFromForm,
} from '../../../../utils/components'
import { useEditorContext } from '../../../EditorContext'
import { NEW_CONFIGURATION_ID } from '../consts'
import { useFormMetaContext } from '../FormMetaContext'
import { useModalContext } from '../ModalContext'
import { getFormData } from '../utils'
import { UseFormHandlers } from './typings'
import {
  getDefaultConfiguration,
  omitUndefined,
  throttledUpdateExtensionFromForm,
} from './utils'
import { createEventObject } from '../../../../utils/auditEvents'

const messages = defineMessages({
  cancel: {
    defaultMessage: 'Cancel',
    id: 'admin/pages.editor.components.modal.form.button.cancel',
  },
  discard: {
    defaultMessage: 'Discard',
    id: 'admin/pages.editor.components.modal.back.button.discard',
  },
  save: {
    defaultMessage: 'Save',
    id: 'admin/pages.editor.components.modal.back.button.save',
  },
  saveError: {
    defaultMessage: 'Something went wrong. Please try again.',
    id: 'admin/pages.editor.components.toast.save.error',
  },
  saveSuccess: {
    defaultMessage: 'Content saved successfully.',
    id: 'admin/pages.editor.components.toast.save.success',
  },
  unsavedBackText: {
    defaultMessage:
      'Your unsaved changes will be lost. Do you want to save them?',
    id: 'admin/pages.editor.components.modal.back.text',
  },
  unsavedListText: {
    defaultMessage:
      'You have unsaved changes. Are you sure you want to continue and discard your modifications? This action cannot be undone.',
    id: 'admin/pages.editor.components.modal.form.text',
  },
  unsavedListTitle: {
    defaultMessage: 'Unsaved changes',
    id: 'admin/pages.editor.components.modal.form.title',
  },
})

export const useFormHandlers: UseFormHandlers = ({
  iframeRuntime,
  intl,
  saveContent,
  sendEventToAudit,
  setState,
  showToast,
  state,
  statusFromRuntime,
}) => {
  const editor = useEditorContext()
  const formMeta = useFormMetaContext()
  const modal = useModalContext()
  const { account, workspace } = useRuntime()

  const handleStatusChange = () => {
    const status = state.status ?? statusFromRuntime

    setState({
      ...state,
      status:
        status === ConfigurationStatus.ACTIVE
          ? ConfigurationStatus.INACTIVE
          : ConfigurationStatus.ACTIVE,
    })

    if (!formMeta.getWasModified()) {
      formMeta.setWasModified(true)
    }
  }

  const handleConditionChange = useCallback(
    (changes: Partial<typeof state['condition']>) => {
      setState({
        condition: {
          ...(state.condition as ExtensionConfiguration['condition']),
          ...changes,
        },
      })

      if (!formMeta.getWasModified()) {
        formMeta.setWasModified(true)
      }
    },
    [formMeta, setState, state]
  )

  const handleFormChange = useCallback(
    event => {
      if (
        state.formData &&
        !formMeta.getWasModified() &&
        !equals(omitUndefined(state.formData), omitUndefined(event.formData))
      ) {
        formMeta.setWasModified(true)
      }

      if (event.formData) {
        setState({ formData: event.formData })

        throttledUpdateExtensionFromForm({
          data: event.formData,
          isContent: true,
          runtime: iframeRuntime,
          treePath: editor.editTreePath,
        })
      }
    },
    [editor.editTreePath, formMeta, iframeRuntime, setState, state]
  )

  const handleFormClose = useCallback(async () => {
    if (!editor.getIsLoading()) {
      editor.setIsLoading(true)
    }

    await iframeRuntime.updateRuntime()

    editor.editExtensionPoint(null)

    editor.setIsLoading(false)
  }, [editor, iframeRuntime])

  const handleFormSave = useCallback(async () => {
    if (editor.getIsLoading()) {
      return
    }

    const content = getSchemaPropsOrContent({
      propsOrContent: state.formData,
      schema: editor.blockData.componentSchema,
    })
    const contentId =
      state.contentId === NEW_CONFIGURATION_ID ? null : state.contentId
    const contentStatus = state.status ?? statusFromRuntime

    const hasOnlyEndDate = state.condition?.statements?.some(
      statement => statement.verb === 'to'
    )
    const isScheduled = state.condition?.statements.length
    const isActive = contentStatus === ConfigurationStatus.ACTIVE
    const isDefault = state.origin

    let newStatus

    if (isActive || hasOnlyEndDate || isDefault) {
      newStatus = ConfigurationStatus.ACTIVE
    } else if (isScheduled) {
      newStatus = ConfigurationStatus.SCHEDULED
    } else {
      newStatus = ConfigurationStatus.INACTIVE
    }

    const newConfiguration = {
      status: newStatus,
      condition: state.condition,
      contentId,
      contentJSON: JSON.stringify(content),
      label: state.label || null,
      origin: isDefault || null,
    }

    const blockId = path<string>(
      ['extensions', editor.editTreePath || '', 'blockId'],
      iframeRuntime
    )

    let error: Error | undefined

    try {
      editor.setIsLoading(true)

      await saveContent({
        variables: {
          bindingId: iframeRuntime.binding?.id,
          blockId,
          configuration: newConfiguration,
          lang: iframeRuntime.culture.locale,
          template: editor.blockData.template,
          treePath: editor.blockData.serverTreePath,
        },
      })

      formMeta.setWasModified(false)

      if (newConfiguration.status === ConfigurationStatus.SCHEDULED) {
        const event = createEventObject(
          'Schedule change',
          'content',
          account,
          workspace,
          contentId
        )
        await sendEventToAudit({
          variables: { input: event },
        })
      }

      const event = createEventObject(
        'Edit content block',
        'content',
        account,
        workspace,
        contentId
      )
      await sendEventToAudit({
        variables: { input: event },
      })

      handleFormClose()
    } catch (err) {
      console.error(err)

      error = err

      editor.setIsLoading(false)
    } finally {
      if (modal.getIsOpen()) {
        modal.close()
      }

      showToast({
        horizontalPosition: 'right',
        message: intl.formatMessage(
          error ? messages.saveError : messages.saveSuccess
        ),
      })
    }
  }, [
    editor,
    state.formData,
    state.contentId,
    state.status,
    state.condition,
    state.origin,
    state.label,
    statusFromRuntime,
    iframeRuntime,
    saveContent,
    sendEventToAudit,
    formMeta,
    handleFormClose,
    modal,
    showToast,
    intl,
  ])

  const handleFormBack = useCallback(() => {
    if (formMeta.getWasModified()) {
      modal.open({
        actionHandler: async () => {
          await handleFormSave()
        },
        cancelHandler: () => {
          if (state.content) {
            updateExtensionFromForm({
              data: state.content,
              isContent: true,
              runtime: iframeRuntime,
              treePath: editor.editTreePath,
            })
          }

          formMeta.setWasModified(false, () => {
            handleFormBack()
          })
        },
        textButtonAction: intl.formatMessage(messages.save),
        textButtonCancel: intl.formatMessage(messages.discard),
        textMessage: intl.formatMessage(messages.unsavedBackText),
        title: intl.formatMessage(messages.unsavedListTitle),
      })
    } else {
      if (modal.getIsOpen()) {
        modal.close()
      }

      handleFormClose()
    }
  }, [
    editor,
    formMeta,
    handleFormClose,
    handleFormSave,
    iframeRuntime,
    intl,
    modal,
    state.content,
  ])

  const handleConfigurationActivate = async (
    configuration: ExtensionConfiguration
  ) => {
    const blockId = path<string>(
      ['extensions', editor.editTreePath || '', 'blockId'],
      iframeRuntime
    )

    let error: Error | undefined

    try {
      editor.setIsLoading(true)

      await saveContent({
        variables: {
          bindingId: iframeRuntime.binding?.id,
          blockId,
          configuration: {
            ...configuration,
            condition: {
              ...configuration.condition,
              statements: [],
            },
            status: ConfigurationStatus.ACTIVE,
          },
          lang: iframeRuntime.culture.locale,
          template: editor.blockData.template,
          treePath: editor.blockData.serverTreePath,
        },
      })

      const event = createEventObject(
        'Activate content block version',
        'content',
        account,
        workspace,
        blockId
      )
      await sendEventToAudit({
        variables: { input: event },
      })
    } catch (err) {
      console.error(err)

      error = err

      editor.setIsLoading(false)
    } finally {
      if (modal.getIsOpen()) {
        modal.close()
      }

      await iframeRuntime.updateRuntime()

      showToast({
        horizontalPosition: 'right',
        message: intl.formatMessage(
          error ? messages.saveError : messages.saveSuccess
        ),
      })

      editor.setIsLoading(false)
    }
  }

  const handleConfigurationOpen = useCallback(
    async (configuration: ExtensionConfiguration) => {
      if (!editor.editTreePath) {
        return
      }

      if (
        configuration.contentId &&
        state.contentId === configuration.contentId
      ) {
        setState({
          mode: state.prevMode,
        })
      } else {
        const baseContent =
          configuration.contentId !== NEW_CONFIGURATION_ID
            ? (JSON.parse(configuration.contentJSON) as Extension['content'])
            : {}

        const formData = getFormData({
          componentImplementation: editor.blockData.componentImplementation,
          content: baseContent,
          contentSchema: editor.blockData.contentSchema,
          iframeRuntime,
        })

        await iframeRuntime.updateExtension(editor.editTreePath, {
          ...iframeRuntime.extensions[editor.editTreePath],
          content: formData,
        })

        const activeContentId = getActiveContentId({
          extensions: iframeRuntime.extensions,
          treePath: editor.editTreePath,
        })

        const newMode =
          configuration.contentId && configuration.contentId === activeContentId
            ? 'editingActive'
            : 'editingInactive'

        setState({
          condition: configuration.condition,
          content: baseContent,
          contentId: configuration.contentId,
          formData,
          label: configuration.label,
          mode: newMode,
          origin: configuration.origin,
        })
      }
    },
    [
      editor.blockData.componentImplementation,
      editor.blockData.contentSchema,
      editor.editTreePath,
      iframeRuntime,
      setState,
      state.contentId,
      state.prevMode,
    ]
  )

  const handleConfigurationCreate = useCallback(
    () =>
      handleConfigurationOpen(
        getDefaultConfiguration({
          iframeRuntime,
          isSitewide: editor.blockData.isSitewide || false,
        })
      ),
    [editor.blockData.isSitewide, handleConfigurationOpen, iframeRuntime]
  )

  const handleLabelChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setState({ label: event.target.value })

      if (!formMeta.getWasModified()) {
        formMeta.setWasModified(true)
      }
    },
    [formMeta, setState]
  )

  const handleListClose = useCallback(() => {
    setState({ mode: state.prevMode })
  }, [setState, state.prevMode])

  const handleListOpen = useCallback(() => {
    if (formMeta.getWasModified()) {
      modal.open({
        actionHandler: async () => {
          editor.setIsLoading(true)

          if (state.content) {
            setState({
              formData: getFormData({
                componentImplementation:
                  editor.blockData.componentImplementation,
                content: state.content,
                contentSchema: editor.blockData.contentSchema,
                iframeRuntime,
              }),
            })
          }

          await iframeRuntime.updateRuntime()

          editor.setIsLoading(false)

          formMeta.setWasModified(false, () => {
            handleListOpen()
          })
        },
        cancelHandler: () => {
          modal.close()
        },
        isActionDanger: true,
        textButtonAction: intl.formatMessage(messages.discard),
        textButtonCancel: intl.formatMessage(messages.cancel),
        textMessage: intl.formatMessage(messages.unsavedListText),
        title: intl.formatMessage(messages.unsavedListTitle),
      })
    } else {
      if (modal.getIsOpen()) {
        modal.close()
      }

      setState({ mode: 'list' })
    }
  }, [editor, formMeta, iframeRuntime, intl, modal, setState, state.content])

  return {
    handleStatusChange,
    handleConditionChange,
    handleConfigurationCreate,
    handleConfigurationOpen,
    handleConfigurationActivate,
    handleFormBack,
    handleFormChange,
    handleFormSave,
    handleLabelChange,
    handleListClose,
    handleListOpen,
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/Transitions/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import Enter from './Enter'
import Exit from './Exit'

const Transitions = {
  Enter,
  Exit,
}

export default Transitions

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/Transitions/typings.d.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { Transition } from 'react-transition-group'

type Direction = 'left' | 'right'

type TransitionProps = React.ComponentPropsWithoutRef<typeof Transition>

interface Props {
  children: React.ReactElement
  condition: TransitionProps['in']
}

export interface EnterProps extends Props {
  from: Direction
}

export interface ExitProps extends Props {
  to: Direction
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/Transitions/consts.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { ANIMATION_TIMEOUT } from '../../../consts'

export const COMMON_PROPS = {
  timeout: ANIMATION_TIMEOUT,
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/BlockSelector/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import type { InjectedIntlProps } from 'react-intl'
import { FormattedMessage, injectIntl } from 'react-intl'

import { getIframeRenderComponents } from '../../../../utils/components'
import { useEditorContext } from '../../../EditorContext'
import {
  getComponents,
  getNormalizedBlocks,
  getTitleByTreePathMap,
} from './utils'
import BlockList from './BlockList'
import styles from '../../EditorContainer.css'

interface Props {
  highlightHandler: (treePath: string | null) => void
  iframeRuntime: RenderContextProps['runtime']
  updateHighlightTitleByTreePath: (
    titleByTreePath?: Record<string, { title?: string; isEditable: boolean }>
  ) => void
}

const BlockSelector: React.FunctionComponent<Props & InjectedIntlProps> = ({
  highlightHandler,
  iframeRuntime,
  intl,
  updateHighlightTitleByTreePath,
}) => {
  const [blocks, setBlocks] = React.useState(() =>
    getNormalizedBlocks({
      extensions: iframeRuntime.extensions,
      page: iframeRuntime.page,
    })
  )

  const editor = useEditorContext()

  const path = React.useRef('')
  const binding = React.useRef('')

  React.useEffect(() => {
    if (
      path.current === iframeRuntime.route.path &&
      binding.current === iframeRuntime.binding?.id
    ) {
      return
    }

    const nextComponents = getComponents(
      iframeRuntime.extensions,
      getIframeRenderComponents(),
      iframeRuntime.page
    )

    const nextBlocks = getNormalizedBlocks(nextComponents)

    setBlocks(nextBlocks)

    updateHighlightTitleByTreePath(getTitleByTreePathMap(nextComponents, intl))

    editor.setIsLoading(false)
    path.current = iframeRuntime.route.path
    binding.current = iframeRuntime.binding.id
  }, [
    editor,
    iframeRuntime.extensions,
    iframeRuntime.page,
    iframeRuntime.route.path,
    iframeRuntime.binding?.id,
    intl,
    updateHighlightTitleByTreePath,
  ])

  const handleMouseEnter = React.useCallback(
    (event: React.MouseEvent<HTMLDivElement | HTMLLIElement>) => {
      const treePath = event.currentTarget.getAttribute('data-tree-path')

      highlightHandler(treePath)
    },
    [highlightHandler]
  )

  const handleMouseLeave = React.useCallback(() => {
    highlightHandler(null)
  }, [highlightHandler])

  return (
    <div className={`w-100 absolute ${styles['h-3em']}`}>
      <h3 className="fw5 ph5 pv4 ma0 lh-copy f5 near-black">
        <FormattedMessage id="admin/pages.editor.components.title" />
      </h3>

      <div className="bb bw1 b--light-silver" />

      <BlockList
        blocks={blocks}
        highlightHandler={highlightHandler}
        onMouseEnterBlock={handleMouseEnter}
        onMouseLeaveBlock={handleMouseLeave}
        iframeRuntime={iframeRuntime}
      />
    </div>
  )
}

export default React.memo(injectIntl(BlockSelector))

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/BlockSelector/utils.test.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import EXTENSIONS from '../__fixtures__/extensions'
import { SidebarComponent } from '../typings'
import COMPONENTS from './__fixtures__/components'
import { NormalizedBlock } from './typings'
import {
  generateWarningMessage,
  getComponents,
  getParentTreePath,
  hasContentPropsInSchema,
  isRootComponent,
  normalize,
} from './utils'

const DEFAULT_EXTENSIONS_COMPONENTS = [
  {
    isEditable: true,
    name: 'admin/editor.carousel.title',
    treePath: 'store.home/carousel#home',
  },
  {
    isEditable: true,
    name: 'admin/editor.shelf.title',
    treePath: 'store.home/shelf#home',
  },
  {
    isEditable: true,
    name: 'admin/editor.productSummary.title',
    treePath: 'store.home/shelf#home/product-summary',
  },
]

describe('getComponents', () => {
  let spiedConsoleWarn: jest.MockInstance<
    ReturnType<Console['warn']>,
    Parameters<Console['warn']>
  >

  beforeEach(() => {
    spiedConsoleWarn = jest
      .spyOn(console, 'warn')
      .mockImplementation(() => undefined)
  })

  afterEach(() => {
    spiedConsoleWarn.mockRestore()
  })

  it('should filter out components without either a schema or a title', () => {
    expect(getComponents(EXTENSIONS, COMPONENTS, 'store.home')).toEqual(
      DEFAULT_EXTENSIONS_COMPONENTS
    )
  })

  it('should use blocks from extension to determine order', () => {
    const swap = <T>(indexA: number, indexB: number, arr: T[]) => {
      const clonedArr = Array.from(arr)

      const temp = clonedArr[indexA]

      clonedArr[indexA] = clonedArr[indexB]

      clonedArr[indexB] = temp

      return clonedArr
    }

    const homeBlocks = EXTENSIONS['store.home'].blocks || []

    const shelfExtensionsIndex = homeBlocks.findIndex(item =>
      item.blockId.endsWith('shelf#home')
    )

    const carouselExtensionsIndex = homeBlocks.findIndex(item =>
      item.blockId.endsWith('carousel#home')
    )

    const reorderedHomeBlocks = swap(
      shelfExtensionsIndex,
      carouselExtensionsIndex,
      homeBlocks
    )

    const shelfComponentsIndex = DEFAULT_EXTENSIONS_COMPONENTS.findIndex(item =>
      item.treePath.endsWith('shelf#home')
    )

    const carouselComponentsIndex = DEFAULT_EXTENSIONS_COMPONENTS.findIndex(
      item => item.treePath.endsWith('carousel#home')
    )

    const reorderedDefaultComponentBlocks = swap(
      shelfComponentsIndex,
      carouselComponentsIndex,
      DEFAULT_EXTENSIONS_COMPONENTS
    )

    const extensions = {
      ...EXTENSIONS,
      'store.home': {
        ...EXTENSIONS['store.home'],
        blocks: reorderedHomeBlocks,
      },
    }

    expect(getComponents(extensions, COMPONENTS, 'store.home')).toEqual(
      reorderedDefaultComponentBlocks
    )
  })

  describe('schema with no titles', () => {
    it('should show extension that have only title in the blocks (extensions) and no schema', () => {
      const extensions: RenderRuntime['extensions'] = {
        ...EXTENSIONS,
        'store.home/title-in-blocks': {
          after: [],
          around: [],
          before: [],
          blockId: 'vtex.title-in-blocks@0.x:title-in-blocks',
          blocks: [],
          component: 'vtex.title-in-blocks@0.0.1/TitleInBlocks',
          composition: 'blocks',
          content: {},
          hasContentSchema: false,
          preview: null,
          props: {},
          render: 'server',
          title: 'admin/title-in-blocks.title',
          track: [],
        },
      }

      expect(getComponents(extensions, {}, 'store.home')).toEqual([
        {
          isEditable: false,
          name: 'admin/title-in-blocks.title',
          treePath: 'store.home/title-in-blocks',
        },
      ])
    })

    it('should filter out components whose extensions have falsy titles', () => {
      const extensions: RenderRuntime['extensions'] = {
        ...EXTENSIONS,
        'store.home/empty-string-title': {
          after: [],
          around: [],
          before: [],
          blockId: 'vtex.empty-string-title@0.x:empty-string-title',
          blocks: [],
          component: 'vtex.empty-string-title@0.0.1/EmptyStringTitle',
          composition: 'blocks',
          content: {},
          hasContentSchema: false,
          preview: null,
          props: {},
          render: 'server',
          track: [],
        },
        'store.home/null-title': {
          after: [],
          around: [],
          before: [],
          blockId: 'vtex.null-title@0.x:null-title',
          blocks: [],
          component: 'vtex.null-title@0.0.1/NullTitle',
          composition: 'blocks',
          content: {},
          hasContentSchema: false,
          preview: null,
          props: {},
          render: 'server',
          track: [],
        },
        'store.home/undefined-title': {
          after: [],
          around: [],
          before: [],
          blockId: 'vtex.undefined-title@0.x:undefined-title',
          blocks: [],
          component: 'vtex.undefined-title@0.0.1/UndefinedTitle',
          composition: 'blocks',
          content: {},
          hasContentSchema: false,
          preview: null,
          props: {},
          render: 'server',
          track: [],
        },
      }

      const components = {
        ...COMPONENTS,
        'vtex.empty-string-title@0.0.1/EmptyStringTitle': {
          schema: {
            title: '',
          },
        },
        'vtex.null-title@0.0.1/NullTitle': {
          schema: {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            title: null as any,
          },
        },
        'vtex.undefined-title@0.0.1/UndefinedTitle': {
          schema: {
            title: undefined,
          },
        },
      }

      expect(getComponents(extensions, components, 'store.home')).toEqual(
        DEFAULT_EXTENSIONS_COMPONENTS
      )
    })
  })

  it('should use getSchema as fallback for schema', () => {
    const components = {
      'vtex.shelf@1.25.0/Shelf': {
        getSchema: () => ({
          properties: { mock: {} },
          title: 'admin/editor.shelf.title',
          type: 'object',
        }),
        schema: undefined,
      },
    }

    expect(getComponents(EXTENSIONS, components, 'store.home')).toEqual([
      {
        isEditable: true,
        name: 'admin/editor.shelf.title',
        treePath: 'store.home/shelf#home',
      },
    ])
  })

  describe('Warnings for schemas without title', () => {
    it('should call console.warn when component has a schema with no title', () => {
      const EXTENSION_ID = 'store.home/shelf#home/product-summary'

      const extensions = {
        [EXTENSION_ID]: EXTENSIONS[EXTENSION_ID],
      }

      const COMPONENT_ID = 'vtex.product-summary@2.31.0/ProductSummaryCustom'

      const components = {
        [COMPONENT_ID]: {
          schema: {},
        },
      }

      expect(getComponents(extensions, components, 'store.home')).toEqual([])

      expect(spiedConsoleWarn).toHaveBeenCalledTimes(1)
      expect(spiedConsoleWarn).toHaveBeenCalledWith(
        generateWarningMessage(COMPONENT_ID)
      )
    })

    it('should call console.warn when component returns a schema from getSchema with no title', () => {
      const COMPONENT_ID = 'vtex.shelf@1.25.0/Shelf'

      const components = {
        ...COMPONENTS,
        [COMPONENT_ID]: {
          getSchema: () => ({}),
        },
      }

      expect(getComponents(EXTENSIONS, components, 'store.home')).toEqual(
        DEFAULT_EXTENSIONS_COMPONENTS.filter(
          item => item.treePath !== 'store.home/shelf#home'
        )
      )

      expect(spiedConsoleWarn).toHaveBeenCalledTimes(1)
      expect(spiedConsoleWarn).toHaveBeenCalledWith(
        generateWarningMessage(COMPONENT_ID)
      )
    })
  })
})

describe('getParentTreePath', () => {
  it('returns parent treePath', () => {
    expect(getParentTreePath('store.home/shelf#home')).toBe('store.home')
  })

  it('handles strings with no delimiter', () => {
    expect(getParentTreePath('store')).toBe('store')
  })
})

describe('hasContentPropsInSchema', () => {
  it(`should return false when schema isn't type object`, () => {
    expect(hasContentPropsInSchema({ title: 'Test' })).toBe(false)
    expect(hasContentPropsInSchema({ type: 'number' })).toBe(false)
  })

  it(`should return true if there are properties with isLayout falsy`, () => {
    expect(
      hasContentPropsInSchema({
        properties: { test: { isLayout: false } },
        type: 'object',
      })
    ).toBe(true)
    expect(
      hasContentPropsInSchema({ type: 'object', properties: { test: {} } })
    ).toBe(true)
  })

  it(`should return true if there are nested properties with isLayout falsy`, () => {
    expect(
      hasContentPropsInSchema({
        properties: { test: { isLayout: false } },
        type: 'object',
      })
    ).toBe(true)
    expect(
      hasContentPropsInSchema({
        properties: { test: { type: 'object', properties: { lala: {} } } },
        type: 'object',
      })
    ).toBe(true)
  })
})

describe('isRootComponent', () => {
  it(`returns true when called with 'store.home/header.full'`, () => {
    const input: SidebarComponent = {
      isEditable: true,
      name: 'header',
      treePath: 'store.home/header.full',
    }

    const expectedOutput = true

    expect(isRootComponent(2)(input)).toBe(expectedOutput)
  })

  it(`returns true when called with 'store.home/footer'`, () => {
    const input: SidebarComponent = {
      isEditable: true,
      name: 'footer',
      treePath: 'store.home/footer',
    }

    const expectedOutput = true

    expect(isRootComponent(2)(input)).toBe(expectedOutput)
  })

  it(`returns true when called with 'store.home/test'`, () => {
    const input: SidebarComponent = {
      isEditable: true,
      name: 'test',
      treePath: 'store.home/test',
    }

    const expectedOutput = true

    expect(isRootComponent(2)(input)).toBe(expectedOutput)
  })

  it(`returns false when called with 'store.home/header/login'`, () => {
    const input: SidebarComponent = {
      isEditable: true,
      name: 'login',
      treePath: 'store.home/header.full/login',
    }

    const expectedOutput = false

    expect(isRootComponent(2)(input)).toBe(expectedOutput)
  })

  it(`returns false when called with 'store.home/shelf/product-summary'`, () => {
    const input: SidebarComponent = {
      isEditable: true,
      name: 'product-summary',
      treePath: 'store.home/shelf/product-summary',
    }

    const expectedOutput = false

    expect(isRootComponent(2)(input)).toBe(expectedOutput)
  })
})

describe('normalize', () => {
  it('handles empty array inputs', () => {
    const input: SidebarComponent[] = []

    const expectedOutput: NormalizedBlock[] = []

    expect(normalize(input)).toEqual(expectedOutput)
  })

  it('nests store.home/ level components', () => {
    const input: SidebarComponent[] = [
      {
        isEditable: true,
        name: 'editor.footer.title',
        treePath: 'store.home/$after_0',
      },
      {
        isEditable: true,
        name: 'editor.header.title',
        treePath: 'store.home/$before_0',
      },
      {
        isEditable: true,
        name: 'editor.category-menu.title',
        treePath: 'store.home/$before_0/category-menu',
      },
      {
        isEditable: true,
        name: 'editor.login.title',
        treePath: 'store.home/$before_0/login',
      },
      {
        isEditable: true,
        name: 'editor.menu',
        treePath: 'store.home/$before_0/menu-link',
      },
      {
        isEditable: true,
        name: 'editor.minicart.title',
        treePath: 'store.home/$before_0/minicart',
      },
      {
        isEditable: true,
        name: 'editor.carousel.title',
        treePath: 'store.home/carousel#home',
      },
      {
        isEditable: true,
        name: 'editor.shelf.title',
        treePath: 'store.home/shelf#home',
      },
    ]

    const expectedOutput: NormalizedBlock[] = [
      {
        components: [
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'editor.category-menu.title',
            treePath: 'store.home/$before_0/category-menu',
          },
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'editor.login.title',
            treePath: 'store.home/$before_0/login',
          },
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'editor.menu',
            treePath: 'store.home/$before_0/menu-link',
          },
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'editor.minicart.title',
            treePath: 'store.home/$before_0/minicart',
          },
        ],
        isEditable: true,
        isSortable: false,
        name: 'editor.header.title',
        treePath: 'store.home/$before_0',
      },
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.carousel.title',
        treePath: 'store.home/carousel#home',
      },
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.shelf.title',
        treePath: 'store.home/shelf#home',
      },
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.footer.title',
        treePath: 'store.home/$after_0',
      },
    ]

    // Don't consider order here
    expect(normalize(input)).toEqual(expect.arrayContaining(expectedOutput))
  })

  it('preserves multilevel structures', () => {
    const input: SidebarComponent[] = [
      {
        isEditable: true,
        name: 'editor.header.title',
        treePath: 'store.home/$before_0',
      },
      {
        isEditable: true,
        name: 'editor.header.title',
        treePath: 'store.home/$before_0/menu',
      },
      {
        isEditable: true,
        name: 'editor.carousel.title',
        treePath: 'store.home/carousel#home',
      },
      {
        isEditable: true,
        name: 'editor.shelf.title',
        treePath: 'store.home/shelf#home',
      },
      {
        isEditable: true,
        name: 'editor.product-summary.title',
        treePath: 'store.home/shelf#home/product-summary',
      },
      {
        isEditable: true,
        name: 'editor.product-rating.title',
        treePath: 'store.home/shelf#home/product-summary/product-rating',
      },
      {
        isEditable: true,
        name: 'editor.product-rating.start.title',
        treePath: 'store.home/shelf#home/product-summary/product-rating/start',
      },
      {
        isEditable: true,
        name: 'editor.footer.title',
        treePath: 'store.home/$after_0',
      },
    ]

    const expectedOutput: NormalizedBlock[] = [
      {
        components: [
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'editor.header.title',
            treePath: 'store.home/$before_0/menu',
          },
        ],
        isEditable: true,
        isSortable: false,
        name: 'editor.header.title',
        treePath: 'store.home/$before_0',
      },
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.carousel.title',
        treePath: 'store.home/carousel#home',
      },
      {
        components: [
          {
            components: [
              {
                components: [
                  {
                    components: [],
                    isEditable: true,
                    isSortable: false,
                    name: 'editor.product-rating.start.title',
                    treePath:
                      'store.home/shelf#home/product-summary/product-rating/start',
                  },
                ],
                isEditable: true,
                isSortable: false,
                name: 'editor.product-rating.title',
                treePath:
                  'store.home/shelf#home/product-summary/product-rating',
              },
            ],
            isEditable: true,
            isSortable: false,
            name: 'editor.product-summary.title',
            treePath: 'store.home/shelf#home/product-summary',
          },
        ],
        isEditable: true,
        isSortable: false,
        name: 'editor.shelf.title',
        treePath: 'store.home/shelf#home',
      },
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.footer.title',
        treePath: 'store.home/$after_0',
      },
    ]

    expect(normalize(input)).toEqual(expectedOutput)
  })

  it('should treat "closest" node as next root', () => {
    const input: SidebarComponent[] = [
      {
        isEditable: true,
        name: 'editor.product-summary.title',
        treePath:
          'store.home/shelf#home/spacer/placeholder/flex/product-summary',
      },
      {
        isEditable: true,
        name: 'editor.product-summary.star.title',
        treePath:
          'store.home/shelf#home/spacer/placeholder/flex/product-summary/star',
      },
      {
        isEditable: true,
        name: 'editor.shelf.title',
        treePath: 'store.home/shelf#home',
      },
    ]

    const expectedOutput: NormalizedBlock[] = [
      {
        components: [
          {
            components: [
              {
                components: [],
                isEditable: true,
                isSortable: false,
                name: 'editor.product-summary.star.title',
                treePath:
                  'store.home/shelf#home/spacer/placeholder/flex/product-summary/star',
              },
            ],
            isEditable: true,
            isSortable: false,
            name: 'editor.product-summary.title',
            treePath:
              'store.home/shelf#home/spacer/placeholder/flex/product-summary',
          },
        ],
        isEditable: true,
        isSortable: false,
        name: 'editor.shelf.title',
        treePath: 'store.home/shelf#home',
      },
    ]

    expect(normalize(input)).toEqual(expectedOutput)
  })

  it('handles unordered components', () => {
    const input: SidebarComponent[] = [
      {
        isEditable: true,
        name: 'editor.footer.title',
        treePath: 'store.home/$after_0',
      },
      {
        isEditable: true,
        name: 'editor.product-summary.title',
        treePath: 'store.home/shelf#home/product-summary',
      },
      {
        isEditable: true,
        name: 'editor.header.title',
        treePath: 'store.home/$before_0',
      },
      {
        isEditable: true,
        name: 'editor.carousel.title',
        treePath: 'store.home/carousel#home',
      },
      {
        isEditable: true,
        name: 'editor.shelf.title',
        treePath: 'store.home/shelf#home',
      },
    ]

    const expectedOutput: NormalizedBlock[] = [
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.header.title',
        treePath: 'store.home/$before_0',
      },
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.carousel.title',
        treePath: 'store.home/carousel#home',
      },
      {
        components: [
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'editor.product-summary.title',
            treePath: 'store.home/shelf#home/product-summary',
          },
        ],
        isEditable: true,
        isSortable: false,
        name: 'editor.shelf.title',
        treePath: 'store.home/shelf#home',
      },
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.footer.title',
        treePath: 'store.home/$after_0',
      },
    ]

    expect(normalize(input)).toEqual(expectedOutput)
  })

  it(`handles components with 'around' blocks`, () => {
    const input: SidebarComponent[] = [
      {
        isEditable: true,
        name: 'editor.header.title',
        treePath: 'store.home/$before_0',
      },
      {
        isEditable: true,
        name: 'editor.carousel.title',
        treePath: 'store.home/carousel#home',
      },
      {
        isEditable: true,
        name: 'editor.product-summary.title',
        treePath: 'store.home/shelf#home/product-summary',
      },
      {
        isEditable: true,
        name: 'editor.around-shelf.title',
        treePath: 'store.home/shelf#home/$around_0',
      },
      {
        isEditable: true,
        name: 'editor.shelf.title',
        treePath: 'store.home/shelf#home',
      },
      {
        isEditable: true,
        name: 'editor.around-carousel.title',
        treePath: 'store.home/carousel#home/$around_0',
      },
      {
        isEditable: true,
        name: 'editor.footer.title',
        treePath: 'store.home/$after_0',
      },
    ]

    const expectedOutput: NormalizedBlock[] = [
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.header.title',
        treePath: 'store.home/$before_0',
      },
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.around-carousel.title',
        treePath: 'store.home/carousel#home/$around_0',
      },
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.carousel.title',
        treePath: 'store.home/carousel#home',
      },
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.around-shelf.title',
        treePath: 'store.home/shelf#home/$around_0',
      },
      {
        components: [
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'editor.product-summary.title',
            treePath: 'store.home/shelf#home/product-summary',
          },
        ],
        isEditable: true,
        isSortable: false,
        name: 'editor.shelf.title',
        treePath: 'store.home/shelf#home',
      },
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.footer.title',
        treePath: 'store.home/$after_0',
      },
    ]

    expect(normalize(input)).toEqual(expectedOutput)
  })

  it('should handle treePaths with same beginning but that are different blocks', () => {
    const input: SidebarComponent[] = [
      {
        isEditable: true,
        name: 'editor.carousel.title',
        treePath: 'store.home/layout#home',
      },
      {
        isEditable: true,
        name: 'editor.product-summary.title',
        treePath: 'store.home/layout#homeCollection',
      },
      {
        isEditable: true,
        name: 'editor.product-summary.title',
        treePath: 'store.home/layout#homeCollection/button',
      },
    ]

    const expectedOutput: NormalizedBlock[] = [
      {
        components: [],
        isEditable: true,
        isSortable: false,
        name: 'editor.carousel.title',
        treePath: 'store.home/layout#home',
      },
      {
        components: [
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'editor.product-summary.title',
            treePath: 'store.home/layout#homeCollection/button',
          },
        ],
        isEditable: true,
        isSortable: false,
        name: 'editor.product-summary.title',
        treePath: 'store.home/layout#homeCollection',
      },
    ]
    const output = normalize(input)

    expect(output).toEqual(expectedOutput)
  })

  it('should put components in closest ancestor', () => {
    const input: SidebarComponent[] = [
      {
        isEditable: true,
        name: 'editor.row.title',
        treePath: 'store.home/flex-layout.row#homeCollections',
      },
      {
        isEditable: true,
        name: 'admin/editor.rich-text.title',
        treePath:
          'store.home/flex-layout.row#homeCollections/flex-layout.col#leftCollection/rich-text#homeCollectionsTitle',
      },
      {
        isEditable: true,
        name: 'admin/editor.info-card.title',
        treePath:
          'store.home/flex-layout.row#homeCollections/flex-layout.col#middleCollection/flex-layout.row#homeCollectionsBottom/info-card#homeBannerDCComics',
      },
      {
        isEditable: true,
        name: 'admin/editor.info-card.title',
        treePath:
          'store.home/flex-layout.row#homeCollections/flex-layout.col#middleCollection/flex-layout.row#homeCollectionsBottom/info-card#homeBannerMarvel',
      },
      {
        isEditable: true,
        name: 'admin/editor.info-card.title',
        treePath:
          'store.home/flex-layout.row#homeCollections/flex-layout.col#middleCollection/flex-layout.row#homeCollectionsTop/info-card#homeBannerAngel',
      },
      {
        isEditable: true,
        name: 'admin/editor.info-card.title',
        treePath:
          'store.home/flex-layout.row#homeCollections/flex-layout.col#middleCollection/flex-layout.row#homeCollectionsTop/info-card#homeBannerSeaHunter',
      },
      {
        isEditable: true,
        name: 'admin/editor.info-card.title',
        treePath:
          'store.home/flex-layout.row#homeCollections/flex-layout.col#rightCollection/info-card#homeCollectionsDisney',
      },
    ]

    const expectedOutput: NormalizedBlock[] = [
      {
        components: [
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'admin/editor.rich-text.title',
            treePath:
              'store.home/flex-layout.row#homeCollections/flex-layout.col#leftCollection/rich-text#homeCollectionsTitle',
          },
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'admin/editor.info-card.title',
            treePath:
              'store.home/flex-layout.row#homeCollections/flex-layout.col#middleCollection/flex-layout.row#homeCollectionsBottom/info-card#homeBannerDCComics',
          },
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'admin/editor.info-card.title',
            treePath:
              'store.home/flex-layout.row#homeCollections/flex-layout.col#middleCollection/flex-layout.row#homeCollectionsBottom/info-card#homeBannerMarvel',
          },
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'admin/editor.info-card.title',
            treePath:
              'store.home/flex-layout.row#homeCollections/flex-layout.col#middleCollection/flex-layout.row#homeCollectionsTop/info-card#homeBannerAngel',
          },
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'admin/editor.info-card.title',
            treePath:
              'store.home/flex-layout.row#homeCollections/flex-layout.col#middleCollection/flex-layout.row#homeCollectionsTop/info-card#homeBannerSeaHunter',
          },
          {
            components: [],
            isEditable: true,
            isSortable: false,
            name: 'admin/editor.info-card.title',
            treePath:
              'store.home/flex-layout.row#homeCollections/flex-layout.col#rightCollection/info-card#homeCollectionsDisney',
          },
        ],
        isEditable: true,
        isSortable: false,
        name: 'editor.row.title',
        treePath: 'store.home/flex-layout.row#homeCollections',
      },
    ]

    expect(normalize(input)).toEqual(expectedOutput)
  })
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/BlockSelector/utils.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { flatten, path, pathOr } from 'ramda'
import { IntlShape } from 'react-intl'
import { formatIOMessage } from 'vtex.native-types'
import { ComponentsRegistry } from 'vtex.render-runtime'

import { getBlockRole } from '../../../../utils/blocks'
import { getIframeRenderComponents } from '../../../../utils/components'
import { ModifiedSidebarComponent, SidebarComponent } from '../typings'
import { BlocksByRole, NormalizedBlock } from './typings'

export const generateWarningMessage = (name: string) =>
  `[Site Editor] Component "${name}" exports schema but doesn't have a "title" property, because of that, it won't appear in the lateral list. If this is intended, ignore this message.`

const getParentContainerBlocksGetter = (extensions: Extensions) => (
  parentPath: string
) => {
  const children = pathOr<InnerBlock[], InnerBlock[]>(
    [],
    [parentPath, 'children'],
    extensions
  )
  return pathOr<InnerBlock[], InnerBlock[]>(
    children,
    [parentPath, 'blocks'],
    extensions
  )
}

const getComponentNameGetterFromExtensions = (extensions: Extensions) => (
  treePath: string
) => (extensions[treePath] && extensions[treePath].component) || ''

const getComponentSchemaGetter = (
  components: ComponentsRegistry | null,
  extensions: Extensions
) => (treePath: string) => {
  const getComponentName = getComponentNameGetterFromExtensions(extensions)

  const extensionProps = pathOr({}, [treePath, 'props'], extensions)

  const component = getComponentName(treePath)

  const getSchema = path([component, 'getSchema'], { ...components })

  return (
    path([component, 'schema'], { ...components }) ||
    (typeof getSchema === 'function' && getSchema(extensionProps))
  )
}

export const isRootComponent = (minimumTreePathSize: number) => (
  component: Pick<ModifiedSidebarComponent, 'treePath'>
) => {
  const splitTreePath = component.treePath.split('/')

  return (
    splitTreePath.length === minimumTreePathSize ||
    (splitTreePath.length === minimumTreePathSize + 1 &&
      splitTreePath[minimumTreePathSize].startsWith('$'))
  )
}

export const hasContentPropsInSchema = (schema: ComponentSchema): boolean => {
  return (
    schema.type === 'object' &&
    Object.values(schema.properties || {}).some(
      property =>
        !property.isLayout ||
        (property.type === 'object' && hasContentPropsInSchema(property))
    )
  )
}

const isSamePage = (page: string, treePath: string) => {
  const pageHead = page && page.split('/')[0]
  const treePathHead = treePath && treePath.split('/')[0]

  return pageHead && treePathHead && treePathHead === pageHead
}

export const getComponents = (
  extensions: Extensions,
  components: ComponentsRegistry | null,
  page: string
) => {
  const getParentContainerProps = getParentContainerBlocksGetter(extensions)

  const getComponentName = getComponentNameGetterFromExtensions(extensions)
  const getComponentSchema = getComponentSchemaGetter(components, extensions)

  const sidebarComponentMap: Record<string, SidebarComponent> = {}
  const isComponentEditableMap: Record<string, boolean> = {}

  return Object.keys(extensions)
    .filter(treePath => {
      const schema = getComponentSchema(treePath)
      const componentName = getComponentName(treePath)
      const hasTitleInSchema = schema && !!schema.title

      if (schema && !hasTitleInSchema) {
        console.warn(generateWarningMessage(componentName))
      }

      const extension = pathOr<Partial<Extension>, Extension>(
        {},
        [treePath],
        extensions
      )

      const hasTitleInBlock = !!extension.title

      const isRoot = isRootComponent(2)({ treePath })

      const isEditable =
        isComponentEditableMap[componentName] !== undefined
          ? isComponentEditableMap[componentName]
          : (isComponentEditableMap[componentName] =
              extension.hasContentSchema || hasContentPropsInSchema(schema))

      const shouldShow =
        isSamePage(page, treePath) &&
        (hasTitleInBlock || (hasTitleInSchema && (isRoot || isEditable)))

      if (shouldShow) {
        sidebarComponentMap[treePath] = {
          isEditable,
          name: extension.title || schema.title,
          treePath,
        }
      }

      return shouldShow
    })
    .sort((treePathA, treePathB) => {
      const splitTreePathA = treePathA.split('/')
      const parentPathA = splitTreePathA
        .slice(0, splitTreePathA.length - 1)
        .join('/')

      const splitTreePathB = treePathB.split('/')
      const parentPathB = splitTreePathB
        .slice(0, splitTreePathB.length - 1)
        .join('/')

      const nameA = splitTreePathA[splitTreePathA.length - 1]
      const nameB = splitTreePathB[splitTreePathB.length - 1]

      if (parentPathA !== parentPathB) {
        return 0
      }

      const parentContainerProps = getParentContainerProps(parentPathA)

      const firstElementPosition = parentContainerProps.findIndex(
        ({ extensionPointId }) => {
          return nameA === extensionPointId
        }
      )
      const secondElementPosition = parentContainerProps.findIndex(
        ({ extensionPointId }) => {
          return nameB === extensionPointId
        }
      )

      const areBothElementsInProps =
        firstElementPosition !== -1 && secondElementPosition !== -1

      if (
        areBothElementsInProps &&
        firstElementPosition > secondElementPosition
      ) {
        return 1
      }

      if (
        areBothElementsInProps &&
        firstElementPosition < secondElementPosition
      ) {
        return -1
      }

      return treePathA > treePathB ? 1 : -1
    })
    .map<SidebarComponent>(treePath => sidebarComponentMap[treePath])
}

const hideNonExistentNodesInTreePath = (
  components: SidebarComponent[]
): ModifiedSidebarComponent[] => {
  const allTreePaths = components.reduce((acc, component) => {
    return acc.add(component.treePath)
  }, new Set())

  return components.map(component => {
    const paths = component.treePath.split('/')

    let prefix: string | null = null
    const existingPaths = paths.filter(path => {
      prefix = prefix === null ? path : [prefix, path].join('/')
      return allTreePaths.has(prefix)
    })

    return {
      ...component,
      modifiedTreePath: existingPaths.join('/'),
    }
  })
}

const modifiedPathLength = (component: ModifiedSidebarComponent) =>
  component.modifiedTreePath.split('/').length

const getComponentRole = (component: SidebarComponent): keyof BlocksByRole => {
  const splitTreePath = component.treePath.split('/')
  const treePathTail = splitTreePath[splitTreePath.length - 1]
  return getBlockRole(treePathTail)
}

export const getParentTreePath = (treePath: string): string => {
  const splitTreePath = treePath.split('/')

  if (splitTreePath.length === 1) {
    return splitTreePath.join('')
  }

  return splitTreePath.slice(0, splitTreePath.length - 1).join('/')
}

const buildTree = (orderedNodes: ModifiedSidebarComponent[]) => {
  const nodes: Record<string, NormalizedBlock> = {}
  orderedNodes.forEach(({ modifiedTreePath, ...component }) => {
    const normalized = {
      ...component,
      components: [],
      isSortable: false,
    }
    const parentId = modifiedTreePath
      .split('/')
      .slice(0, -1)
      .join('/')

    nodes[modifiedTreePath] = normalized

    if (nodes[parentId] != null) {
      const parent = nodes[parentId]
      parent.components.push(normalized)
    }
  })
  return nodes
}

const partitionNodesChildrenByRole = (
  nodes: Record<string, NormalizedBlock>
) => {
  const partitionChildrenByRole = ({ components }: NormalizedBlock) =>
    components.reduce(
      (componentsByRole: BlocksByRole, component) => {
        componentsByRole[getComponentRole(component)].push(component)
        return componentsByRole
      },
      { around: [], after: [], before: [], blocks: [] }
    )

  return Object.values(nodes).reduce(
    (childrenByRole: Record<string, BlocksByRole>, node) => {
      childrenByRole[node.treePath] = partitionChildrenByRole(node)
      return childrenByRole
    },
    {}
  )
}
const hoistSubtrees = (
  component: NormalizedBlock,
  pathToChildrenByRole: Record<string, BlocksByRole>,
  isRoot = false
): NormalizedBlock[] => {
  const { treePath, components } = component
  const childrenByRole = pathToChildrenByRole[treePath]
  const blockChildren = components.filter(
    child => getComponentRole(child) === 'blocks'
  )
  const hoistedComponents = flatten(
    blockChildren.map(child => hoistSubtrees(child, pathToChildrenByRole))
  )
  const current = isRoot
    ? hoistedComponents
    : [{ ...component, components: hoistedComponents }]

  if (childrenByRole) {
    return [
      ...childrenByRole.around,
      ...childrenByRole.before,
      ...current,
      ...childrenByRole.after,
    ]
  }

  return [...current]
}

const hoistSurroundingBlocks = (
  nodes: Record<string, NormalizedBlock>,
  rootId: string
) => {
  const childrenByRole = partitionNodesChildrenByRole(nodes)

  return hoistSubtrees(nodes[rootId], childrenByRole, true)
}

export const normalize = (components: SidebarComponent[]) => {
  if (components.length === 0) {
    return []
  }
  const rootId = components[0].treePath.split('/')[0]
  const root = { treePath: rootId, name: 'root', isEditable: false }
  const allComponents = [root, ...components]

  const modifiedComponents = hideNonExistentNodesInTreePath(allComponents).sort(
    (cmp1: ModifiedSidebarComponent, cmp2: ModifiedSidebarComponent) =>
      modifiedPathLength(cmp1) - modifiedPathLength(cmp2)
  )

  const nodes = buildTree(modifiedComponents)

  return hoistSurroundingBlocks(nodes, rootId)
}

export const getNormalizedBlocks = (
  partialContextOrComponents:
    | Pick<RenderContext, 'extensions' | 'page'>
    | ReturnType<typeof getComponents>
) => {
  if (
    'extensions' in partialContextOrComponents &&
    'page' in partialContextOrComponents
  ) {
    const partialContext = partialContextOrComponents as Pick<
      RenderContext,
      'extensions' | 'page'
    >
    return normalize(
      getComponents(
        partialContext.extensions,
        getIframeRenderComponents(),
        partialContext.page
      )
    )
  }

  const components = partialContextOrComponents as ReturnType<
    typeof getComponents
  >
  return normalize(components)
}

export const getTitleByTreePathMap = (
  components: SidebarComponent[],
  intl: IntlShape
) => {
  return components.reduce<
    Record<string, { title?: string; isEditable: boolean }>
  >((acc, { isEditable, name, treePath }) => {
    const title = formatIOMessage({ id: name, intl })
    acc[treePath] = { title, isEditable }

    return acc
  }, {})
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/BlockSelector/typings.d.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { SidebarComponent } from '../typings'

export interface NormalizedBlock extends SidebarComponent {
  components: NormalizedBlock[]
  isEditable: boolean
  isSortable: boolean
}

export interface BlocksByRole {
  after: NormalizedBlock[]
  around: NormalizedBlock[]
  before: NormalizedBlock[]
  blocks: NormalizedBlock[]
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationList/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { defineMessages, injectIntl } from 'react-intl'

import { getActiveContentId } from '../../../../../utils/components'
import { useEditorContext } from '../../../../EditorContext'
import { EditingState } from '../../typings'
import EditorHeader from '../EditorHeader'
import { getIsDefaultContent } from '../utils'
import Card from './Card'
import CreateButton from './CreateButton'
import { useListHandlers } from './hooks'
import { UseListHandlersParams } from './typings'
import { getConfigurationSort } from './utils'

interface Props extends Omit<UseListHandlersParams, 'activeContentId'> {
  editingContentId: EditingState['contentId']
  onConfigurationCreate: () => void
  onConfigurationOpen: (configuration: ExtensionConfiguration) => void
  onListClose?: () => void
  onConfigurationActivate: (
    configuration: ExtensionConfiguration
  ) => Promise<void>
  onListOpen?: () => void
}

const messages = defineMessages({
  contentCards: {
    defaultMessage: 'Configurations',
    id: 'admin/pages.editor.component-list.title',
  },
})

const BlockConfigurationList: React.FC<Props> = ({
  deleteContent,
  sendEventToAudit,
  editingContentId,
  iframeRuntime,
  intl,
  onConfigurationCreate,
  onConfigurationOpen,
  onConfigurationActivate,
  onListClose,
  onListOpen,
  showToast,
}) => {
  const editor = useEditorContext()

  const activeContentId = React.useMemo(
    () =>
      getActiveContentId({
        extensions: iframeRuntime.extensions,
        treePath: editor.editTreePath,
      }),
    [editor.editTreePath, iframeRuntime.extensions]
  )

  const { handleConfirmConfigurationDelete } = useListHandlers({
    activeContentId,
    deleteContent,
    sendEventToAudit,
    iframeRuntime,
    intl,
    showToast,
  })

  if (!editor.blockData.configurations) {
    return null
  }

  const configurations = editor.blockData.configurations.sort(
    getConfigurationSort(activeContentId)
  )

  return (
    <div
      className="w-100 min-h-100 absolute bg-white"
      style={{ boxShadow: '-3px 0px 23px 0px rgba(0,0,0,0.14)' }}
    >
      <EditorHeader
        onListClose={onListClose}
        onListOpen={onListOpen}
        title={intl.formatMessage(messages.contentCards)}
      />

      <CreateButton onClick={onConfigurationCreate} />

      {configurations.map((configuration: ExtensionConfiguration, index) => (
        <Card
          configuration={configuration}
          isActive={configuration.contentId === activeContentId}
          isDefaultContent={getIsDefaultContent(configuration)}
          isEditing={editingContentId === configuration.contentId}
          key={configuration.contentId || index}
          onConfigurationOpen={onConfigurationOpen}
          onConfigurationActivate={onConfigurationActivate}
          onDelete={handleConfirmConfigurationDelete}
        />
      ))}
    </div>
  )
}

export default injectIntl(BlockConfigurationList)

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of admin-pages/react/components/EditorContainer/Sidebar/BlockEditor/BlockConfigurationList/utils.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import ListContent from '../../../graphql/ListContent.graphql'
import { ListContentData } from '../../../queries/ListContent'
import { GetDeleteStoreUpdater } from './typings'

export const isConfigurationExpired = (
  configuration: ExtensionConfiguration
) => {
  const endDateStatement = configuration.condition.statements.find(
    (statement: ExtensionConfigurationConditionStatement) =>
      statement &&
      statement.subject === 'date' &&
      JSON.parse(statement.objectJSON).to
  )

  const getEndDate = (statement: ExtensionConfigurationConditionStatement) => {
    return new Date(JSON.parse(statement.objectJSON).to)
  }

  if (endDateStatement && getEndDate(endDateStatement) < new Date()) {
    return true
  }

  return false
}

export const getConfigurationSort = (activeContentId: string) => (
  current: ExtensionConfiguration,
  previous: ExtensionConfiguration
) => {
  const isActiveConfiguration = (configuration: ExtensionConfiguration) =>
    configuration.contentId === activeContentId

  const isAppConfiguration = (configuration: ExtensionConfiguration) =>
    configuration.origin

  if (isActiveConfiguration(current)) {
    return -1
  }

  if (isAppConfiguration(current)) {
    return 1
  }

  if (isAppConfiguration(previous)) {
    return -1
  }

  if (isConfigurationExpired(previous)) {
    return -1
  }

  if (isConfigurationExpired(current)) {
    return 1
  }

  return 0
}

export const isConfigurationScheduled = (
  configuration: ExtensionConfiguration
) => configuration.condition.statements.length > 0

export const getDeleteStoreUpdater: GetDeleteStoreUpdater = ({
  action,
  blockId,
  iframeRuntime,
  serverTreePath,
  setBlockData,
  template,
}) => (store, { data }) => {
  if (action === 'reset') {
    return
  }

  const cacheAccessParameters = {
    query: ListContent,
    variables: {
      bindingId: iframeRuntime.binding?.id,
      blockId,
      pageContext: iframeRuntime.route.pageContext,
      template,
      treePath: serverTreePath,
    },
  }

  const deletedContentId = data && data.deleteContent

  try {
    const queryData = store.readQuery<ListContentData>(cacheAccessParameters)

    if (queryData) {
      const content =
        queryData.listContentWithSchema &&
        queryData.listContentWithSchema.content

      const newContent =
        content &&
        content.filter(({ contentId }) => contentId !== deletedContentId)

      const newData = {
        ...queryData,
        listContentWithSchema: {
          ...queryData.listContentWithSchema,
          content: newContent,
        },
      }

      store.writeQuery({
        data: newData,
        ...cacheAccessParameters,
      })

      setBlockData({ configurations: newContent })
    }
  } catch (err) {
    console.warn('No cache found for "ListContent".')
  }
}

```