[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/admin/redirects/Form/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import classnames from 'classnames'
import moment from 'moment'
import PropTypes from 'prop-types'
import React, { Component, Fragment } from 'react'
import {
  defineMessages,
  FormattedMessage,
  injectIntl,
  WrappedComponentProps as ComponentWithIntlProps,
} from 'react-intl'
import { withRuntimeContext } from 'vtex.render-runtime'
import {
  Button,
  DatePicker,
  Dropdown,
  IconClose,
  Input,
  RadioGroup,
  ToastConsumerFunctions,
  Toggle,
} from 'vtex.styleguide'
import { Binding } from 'vtex.tenant-graphql'

import { getBindingSelectorOptions } from '../../../../utils/bindings'
import { DropdownChangeInput } from '../../../../utils/bindings/typings'
import Modal from '../../../Modal'
import FormFieldSeparator from '../../FormFieldSeparator'
import { BASE_URL, NEW_REDIRECT_ID } from '../consts'
import { DeleteRedirectMutationFn, SaveRedirectMutationFn } from './typings'

interface CustomProps {
  initialData: Redirect
  onDelete: DeleteRedirectMutationFn
  onSave: SaveRedirectMutationFn
  storeBindings: Binding[] | null
  isNew: boolean
}

type Props = CustomProps &
  ComponentWithIntlProps &
  RenderContextProps &
  Pick<ToastConsumerFunctions, 'showToast'>

interface State {
  data: Redirect
  isLoading: boolean
  shouldShowDatePicker: boolean
  shouldShowModal: boolean
}

const messages = defineMessages({
  buttonCancel: {
    defaultMessage: 'Cancel',
    id: 'admin/pages.admin.redirects.form.button.cancel',
  },
  buttonRemove: {
    defaultMessage: 'Remove',
    id: 'admin/pages.admin.redirects.form.button.remove',
  },
  deleteError: {
    defaultMessage: 'Error: redirect could not be deleted.',
    id: 'admin/pages.admin.redirects.form.delete.error',
  },
  from: {
    defaultMessage: 'From',
    id: 'admin/pages.admin.redirects.table.from',
  },
  modalText: {
    defaultMessage: 'Are you sure you want to remove this redirect?',
    id: 'admin/pages.admin.redirects.form.modal.text',
  },
  permanent: {
    defaultMessage: 'This redirect is permanent (Code: 301).',
    id: 'admin/pages.admin.redirects.form.toggle.permanent',
  },
  saveError: {
    defaultMessage: 'Error: redirect could not be saved.',
    id: 'admin/pages.admin.redirects.form.save.error',
  },
  temporary: {
    defaultMessage: 'This redirect is temporary (Code: 302).',
    id: 'admin/pages.admin.redirects.form.toggle.temporary',
  },
  to: {
    defaultMessage: 'To',
    id: 'admin/pages.admin.redirects.table.to',
  },
  toggleEndDate: {
    defaultMessage: 'This redirect has an end date',
    id: 'admin/pages.admin.redirects.form.toggle.endDate',
  },
  bindingSelectorTitle: {
    defaultMessage: 'Binding',
    id: 'admin/pages.admin.pages.form.binding-selector.title',
  },
})

interface BindingSelectorWrapper {
  visible: boolean
}

const BindingSelectorWrapper: React.FunctionComponent<BindingSelectorWrapper> = props => {
  const { children, visible } = props
  return visible ? (
    <div>
      <FormFieldSeparator />
      {children}
      <div className="mb7 pb7 bb bw1 b--light-silver f3 normal" />
      <FormFieldSeparator />
    </div>
  ) : null
}

class Form extends Component<Props, State> {
  public static contextTypes = {
    culture: PropTypes.shape({ locale: PropTypes.string.isRequired })
      .isRequired,
  }

  private isEditingRedirect: boolean
  private minDate = new Date()

  public constructor(props: Props) {
    super(props)

    this.isEditingRedirect = props.initialData.from !== NEW_REDIRECT_ID

    this.state = {
      data: props.initialData,
      isLoading: false,
      shouldShowDatePicker: !!props.initialData.endDate,
      shouldShowModal: false,
    }
  }

  public componentDidMount() {
    window.top.postMessage({ action: { type: 'STOP_LOADING' } }, '*')
  }

  public render() {
    const { locale } = this.context.culture
    const { intl, storeBindings, isNew } = this.props
    const {
      data,
      isLoading,
      shouldShowModal,
      shouldShowDatePicker,
    } = this.state

    const bindingOptions =
      storeBindings && getBindingSelectorOptions(storeBindings)

    return (
      <Fragment>
        <Modal
          isActionDanger
          isActionLoading={isLoading}
          isOpen={shouldShowModal}
          onClickAction={this.handleDelete(data.from, data.binding)}
          onClickCancel={this.handleModalVisibilityToggle}
          onClose={this.handleModalVisibilityToggle}
          textButtonAction={intl.formatMessage(messages.buttonRemove)}
          textButtonCancel={intl.formatMessage(messages.buttonCancel)}
          textMessage={intl.formatMessage(messages.modalText)}
        />
        <h1>
          {this.isEditingRedirect ? (
            <FormattedMessage
              id="admin/pages.admin.redirects.form.title.info"
              defaultMessage="Redirect info"
            />
          ) : (
            <FormattedMessage
              id="admin/pages.admin.redirects.form.title.new"
              defaultMessage="Create a redirect"
            />
          )}
        </h1>
        <FormFieldSeparator />
        {storeBindings && (
          <BindingSelectorWrapper visible={storeBindings.length > 1}>
            <Dropdown
              label={intl.formatMessage(messages.bindingSelectorTitle)}
              disabled={!isNew || storeBindings.length === 1}
              onChange={this.handleBindingChange}
              options={bindingOptions}
              value={data.binding || storeBindings[0]}
            />
          </BindingSelectorWrapper>
        )}
        <form onSubmit={this.handleSave}>
          <Input
            label={intl.formatMessage(messages.from)}
            onChange={this.handleFromUpdate}
            required
            value={data.from}
          />
          <FormFieldSeparator />
          <Input
            label={intl.formatMessage(messages.to)}
            onChange={this.handleToUpdate}
            required
            value={data.to}
          />
          <FormFieldSeparator />
          <RadioGroup
            name="type"
            options={[
              {
                label: intl.formatMessage(messages.permanent),
                value: 'PERMANENT',
              },
              {
                label: intl.formatMessage(messages.temporary),
                value: 'TEMPORARY',
              },
            ]}
            value={data.type}
            onChange={this.handleTypeChange}
          />
          <FormFieldSeparator />
          {data.type === 'TEMPORARY' ? (
            <>
              <div className="relative">
                <Toggle
                  checked={shouldShowDatePicker}
                  label={intl.formatMessage(messages.toggleEndDate)}
                  onChange={this.handleDatePickerVisibilityToggle}
                />
              </div>
              <FormFieldSeparator />
              {shouldShowDatePicker && (
                <Fragment>
                  <FormattedMessage id="admin/pages.admin.redirects.form.datePicker.title">
                    {text => <div className="mb3 w-100 f6">{text}</div>}
                  </FormattedMessage>
                  <div className="flex">
                    <DatePicker
                      useTime
                      direction="up"
                      locale={locale}
                      onChange={this.handleEndDateUpdate}
                      minDate={this.minDate}
                      value={
                        data.endDate ? moment(data.endDate).toDate() : undefined
                      }
                    />
                    <button
                      type="button"
                      className="flex items-center justify-center bn input-reset near-black"
                      onClick={this.handleEndDateClear}
                    >
                      <IconClose />
                    </button>
                  </div>
                  <FormFieldSeparator />
                </Fragment>
              )}
            </>
          ) : null}
          <div
            className={classnames('justify-between', {
              flex: this.isEditingRedirect,
            })}
          >
            {this.isEditingRedirect ? (
              <Button
                size="small"
                onClick={this.handleModalVisibilityToggle}
                variation="danger"
              >
                <FormattedMessage
                  id="admin/pages.admin.redirects.form.button.remove"
                  defaultMessage="Remove"
                />
              </Button>
            ) : null}
            <div className="flex justify-end">
              <div className="mr6">
                <Button
                  disabled={isLoading}
                  onClick={this.handleExit}
                  size="small"
                  variation="tertiary"
                >
                  {this.isEditingRedirect ? (
                    <FormattedMessage
                      id="admin/pages.admin.redirects.form.button.back"
                      defaultMessage="Back"
                    />
                  ) : (
                    <FormattedMessage
                      id="admin/pages.admin.redirects.form.button.cancel"
                      defaultMessage="Cancel"
                    />
                  )}
                </Button>
              </div>
              <Button isLoading={isLoading} size="small" type="submit">
                <FormattedMessage
                  id="admin/pages.admin.redirects.form.button.save"
                  defaultMessage="Save"
                />
              </Button>
            </div>
          </div>
        </form>
      </Fragment>
    )
  }

  private handleExit = () => {
    this.props.runtime.navigate({ to: BASE_URL })
  }

  private handleDelete = (
    redirectFrom: string,
    redirectBinding: string
  ) => () => {
    const { intl, onDelete } = this.props

    this.setState({ isLoading: true }, async () => {
      try {
        await onDelete({
          variables: {
            path: redirectFrom,
            binding: redirectBinding,
          },
        })

        this.handleExit()
      } catch (err) {
        this.setState({ isLoading: false }, () => {
          console.error(err)

          this.props.showToast({
            horizontalPosition: 'right',
            message: intl.formatMessage(messages.deleteError),
          })
        })
      }
    })
  }

  private handleInputChange = (inputData: object) => {
    this.setState(prevState => ({
      ...prevState,
      data: {
        ...prevState.data,
        ...inputData,
      },
    }))
  }

  private handleSave = (event: React.FormEvent) => {
    const { intl, onSave } = this.props
    const {
      data: { endDate, from, to, type, binding },
    } = this.state

    event.preventDefault()

    this.setState({ isLoading: true }, async () => {
      try {
        await onSave({
          variables: {
            endDate,
            from,
            to,
            type,
            binding,
          },
        })

        this.handleExit()
      } catch (err) {
        this.setState({ isLoading: false }, () => {
          console.error(err)

          this.props.showToast({
            horizontalPosition: 'right',
            message: intl.formatMessage(messages.saveError),
          })
        })
      }
    })
  }

  private handleDatePickerVisibilityToggle = () => {
    this.setState(
      prevState => ({
        ...prevState,
        shouldShowDatePicker: !prevState.shouldShowDatePicker,
      }),
      () => {
        this.handleInputChange({
          endDate: this.state.shouldShowDatePicker
            ? moment(this.props.initialData.endDate || undefined).add(1, 'days')
            : '',
        })
      }
    )
  }

  private handleModalVisibilityToggle = () => {
    this.setState(prevState => ({
      ...prevState,
      shouldShowModal: !prevState.shouldShowModal,
    }))
  }

  private handleTypeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const type = e.target.value
    this.handleInputChange({
      ...(type === 'PERMANENT' ? { endDate: '' } : null),
      type,
    })
  }

  private handleEndDateUpdate = (value: Date) => {
    this.handleInputChange({ endDate: value })
  }

  private handleEndDateClear = () => {
    this.handleInputChange({ endDate: '' })
  }

  private handleFromUpdate = (event: Event) => {
    if (event.target instanceof HTMLInputElement) {
      this.handleInputChange({ from: event.target.value })
    }
  }

  private handleToUpdate = (event: Event) => {
    if (event.target instanceof HTMLInputElement) {
      this.handleInputChange({ to: event.target.value })
    }
  }

  private handleBindingChange = ({
    target: { value },
  }: DropdownChangeInput) => {
    const { storeBindings } = this.props
    const { data } = this.state
    if (!storeBindings) {
      return
    }
    const selectedBinding = storeBindings.find(binding => binding.id === value)
    if (!selectedBinding) {
      throw new Error(
        "Selected binding does not exist or isn't a store binding"
      )
    }
    data.binding = selectedBinding.id
    this.setState({ data })
  }
}

export default withRuntimeContext(injectIntl(Form))

```


[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/admin/redirects/Form/utils.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { DataProxy } from 'apollo-cache'

import Redirect from '../../../../queries/Redirect.graphql'
import Redirects from '../../../../queries/Redirects.graphql'
import { PAGINATION_START, PAGINATION_STEP } from '../consts'
import { RedirectsQuery } from '../typings'
import { QueryData, StoreUpdaterGetter } from './typings'

const cacheAccessParameters = {
  query: Redirects,
  variables: {
    from: PAGINATION_START,
    to: PAGINATION_START + PAGINATION_STEP,
  },
}

const readRedirectsFromStore = (store: DataProxy): QueryData =>
  store.readQuery(cacheAccessParameters)

const writeRedirectsToStore = (newData: RedirectsQuery, store: DataProxy) => {
  store.writeQuery({
    data: newData,
    ...cacheAccessParameters,
  })
}

export const getStoreUpdater: StoreUpdaterGetter = operation => (
  store,
  result
) => {
  const deleteRedirect = result.data?.redirect?.delete
  const saveRedirect = result.data?.redirect?.save

  const isDelete = operation === 'delete'

  try {
    const queryData = readRedirectsFromStore(store)

    if (queryData) {
      const newRedirects =
        (isDelete
          ? deleteRedirect &&
            queryData.redirect.list.filter(
              redirect => redirect.from !== deleteRedirect.from
            )
          : saveRedirect &&
            queryData.redirect.list.reduce(
              (acc, currRedirect) =>
                currRedirect.from === saveRedirect.from &&
                currRedirect.binding === saveRedirect.binding
                  ? acc
                  : [...acc, currRedirect],
              [saveRedirect]
            )) || queryData.redirect.list

      const newData = {
        ...queryData,
        redirect: {
          ...queryData.redirect,
          list: newRedirects,
          numberOfEntries: isDelete
            ? queryData.redirect.numberOfEntries - 1
            : queryData.redirect.numberOfEntries + 1,
        },
      }

      writeRedirectsToStore(newData, store)
    }
  } catch (err) {
    console.warn('No cache found for "Redirects".')
  }

  try {
    const variables = isDelete
      ? deleteRedirect && {
          path: deleteRedirect.from,
          binding: deleteRedirect.binding,
        }
      : saveRedirect && {
          path: saveRedirect.from,
          binding: saveRedirect.binding,
        }

    store.writeQuery({
      data: { redirect: isDelete ? undefined : { save: saveRedirect } },
      query: Redirect,
      variables,
    })
  } catch (e) {
    console.error('Error writing to "Redirect".')
  }
}

```


[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/admin/redirects/Form/typings.d.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { MutationFn, MutationUpdaterFn } from 'react-apollo'

import { RedirectsQuery } from '../typings'

export interface MutationResult {
  data?: {
    [name: string]: Redirect
  }
}

export type QueryData = RedirectsQuery | null

export interface RedirectQuery {
  redirect: {
    get: Redirect
  }
}

export interface DeleteRedirectVariables {
  path: string
  binding?: string
}

export interface SaveRedirectVariables {
  id?: string
  endDate: string | null
  from: string
  to: string
  type: RedirectTypes
  binding?: string
}

interface Mutations {
  redirect?: {
    delete?: Redirect
    save?: Redirect
  }
}

export type StoreUpdaterGetter = (
  operation: 'delete' | 'save'
) => MutationUpdaterFn<Mutations>

export interface DeleteRedirectData {
  redirect: {
    delete: Redirect
  }
}

export type DeleteRedirectMutationFn = MutationFn<
  DeleteRedirectData,
  DeleteRedirectVariables
>

export interface SaveRedirectData {
  redirect: {
    save: Redirect
  }
}

export type SaveRedirectMutationFn = MutationFn<
  SaveRedirectData,
  SaveRedirectVariables
>

```


[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/admin/redirects/Form/Operations.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { Mutation } from 'react-apollo'

import DeleteRedirect from '../../../../queries/DeleteRedirect.graphql'
import SaveRedirect from '../../../../queries/SaveRedirect.graphql'

import {
  DeleteRedirectData,
  DeleteRedirectMutationFn,
  DeleteRedirectVariables,
  SaveRedirectData,
  SaveRedirectMutationFn,
  SaveRedirectVariables,
} from './typings'
import { getStoreUpdater } from './utils'

interface Props {
  children: (mutations: OperationsObj) => React.ReactNode
}

interface OperationsObj {
  deleteRedirect: DeleteRedirectMutationFn
  saveRedirect: SaveRedirectMutationFn
}

const Operations = (props: Props) => (
  <Mutation<DeleteRedirectData, DeleteRedirectVariables>
    mutation={DeleteRedirect}
    update={getStoreUpdater('delete')}
  >
    {deleteRedirect => (
      <Mutation<SaveRedirectData, SaveRedirectVariables>
        mutation={SaveRedirect}
        update={getStoreUpdater('save')}
      >
        {saveRedirect =>
          props.children({
            deleteRedirect,
            saveRedirect,
          })
        }
      </Mutation>
    )}
  </Mutation>
)

export default Operations

```


[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/admin/redirects/UploadModal/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import classnames from 'classnames'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Mutation } from 'react-apollo'
import { FormattedMessage, useIntl } from 'react-intl'
import { Modal } from 'vtex.styleguide'

import SaveRedirectFromFileMutation, {
  MutationRenderProps,
  UploadActionType,
} from '../mutations/SaveRedirectFromFile'
import Loading from './Loading'
import UploadError from './UploadError'
import UploadPrompt from './UploadPrompt'
import {
  DeleteManyRedirectsProps,
  DeleteManyRedirectsFromFileResult,
  DeleteManyRedirectsFromFileVariables,
} from '../mutations/DeleteManyRedirectsFromFile'
import DeleteManyRedirects from '../mutations/DeleteManyRedirectsFromFile.graphql'
import { ModalStates } from './typings'
import { getAlertState } from './UploadPrompt/getAlertState'
import { AlertState } from '../typings'
import bulkUploadRedirects from '../bulkUploadRedirects'

interface RouteLocator {
  from: string
  binding: string
}
interface Props {
  hasRedirects: boolean
  isOpen: boolean
  onClose: () => void
  onDownloadTemplate: () => void
  refetchRedirects: () => void
  setAlert: (alertState: AlertState) => void
  hasMultipleBindings: boolean
}

const INITIAL_MODAL_STATE = 'UPLOAD_FILE'

const UploadModal: React.FunctionComponent<Props &
  MutationRenderProps &
  DeleteManyRedirectsProps> = ({
  deleteManyRedirects,
  error,
  hasRedirects,
  isOpen,
  onClose,
  onDownloadTemplate,
  refetchRedirects,
  saveRedirectFromFile,
  setAlert,
  hasMultipleBindings,
}) => {
  const intl = useIntl()
  const [currentStep, setModalStep] = useState<ModalStates>(INITIAL_MODAL_STATE)

  const [processedRedirect, setProcessedRedirect] = useState<number>(0)
  const [numberOfRedirects, setNumberOfRedirects] = useState<number>(-1)
  const shouldUploadRef = useRef<boolean>(false)

  useEffect(() => {
    shouldUploadRef.current = isOpen
  }, [isOpen])

  const [uploadActionType, setUploadActionType] = useState<UploadActionType>(
    'save'
  )

  const resetState = useCallback(
    async (refetch = false) => {
      onClose()
      setModalStep(INITIAL_MODAL_STATE)
      setUploadActionType('save')
      setProcessedRedirect(0)
      setNumberOfRedirects(-1)
      if (refetch) {
        await refetchRedirects()
      }
    },
    [onClose, refetchRedirects]
  )

  const handlePromptCancel = useCallback(() => {
    resetState(false)
  }, [resetState])

  const saveRedirectFromFileCb = useCallback(
    async (
      parsedJsonFromCsv: Redirect[],
      fileName: string,
      hasMultipleBindings: boolean
    ) => {
      shouldUploadRef.current = true
      const isSave = uploadActionType === 'save'
      const mutation = (data: Redirect[]) => {
        if (isSave) {
          return saveRedirectFromFile({
            variables: { redirects: data as Redirect[] },
          })
        }
        const { paths, locators } = data.reduce(
          (acc, redirect) => {
            acc.paths.push(redirect.from)
            acc.locators.push({
              from: redirect.from,
              binding: redirect.binding,
            })
            return acc
          },
          {
            paths: [] as string[],
            locators: [] as RouteLocator[],
          }
        )
        const variables = hasMultipleBindings
          ? {
              paths,
              locators,
            }
          : { paths }
        return deleteManyRedirects({
          variables,
        })
      }

      try {
        const numberOfLines = parsedJsonFromCsv.length

        setNumberOfRedirects(numberOfLines)
        setModalStep('LOADING')

        const { failedRedirects } = await bulkUploadRedirects({
          data: parsedJsonFromCsv,
          isSave,
          mutation,
          shouldUploadRef,
          updateProgress: processed => {
            setProcessedRedirect(processed)
          },
        })

        if (shouldUploadRef.current) {
          setAlert(
            getAlertState({
              total: numberOfLines,
              failedRedirects,
              intl,
              fileName,
              mutation,
              isSave,
            })
          )
          resetState(true)
        }
      } catch (e) {
        setModalStep('ERROR')
      }
    },
    [
      deleteManyRedirects,
      intl,
      resetState,
      saveRedirectFromFile,
      setAlert,
      uploadActionType,
    ]
  )

  const CurrentComponent = useMemo(() => {
    switch (currentStep) {
      case 'UPLOAD_FILE':
        return (
          <UploadPrompt
            setUploadActionType={setUploadActionType}
            hasRedirects={hasRedirects}
            onCancel={handlePromptCancel}
            onDownloadTemplate={onDownloadTemplate}
            saveRedirectFromFile={saveRedirectFromFileCb}
            uploadActionType={uploadActionType}
            hasMultipleBindings={hasMultipleBindings}
          />
        )
      case 'LOADING':
        return (
          <Loading
            current={processedRedirect}
            total={numberOfRedirects}
            onCancel={() => {
              setModalStep('UPLOAD_FILE')
              shouldUploadRef.current = false
            }}
          />
        )
      case 'ERROR':
        return <UploadError error={error} />
      default:
        return null
    }
  }, [
    currentStep,
    error,
    handlePromptCancel,
    hasRedirects,
    numberOfRedirects,
    onDownloadTemplate,
    processedRedirect,
    saveRedirectFromFileCb,
    uploadActionType,
    hasMultipleBindings,
  ])

  return (
    <Modal centered isOpen={isOpen} onClose={resetState}>
      <h1 className="f3 fw4 mt0 mb6">
        <FormattedMessage id="admin/pages.admin.redirects.upload-modal.prompt.select-action.title" />
      </h1>
      <div
        className={classnames('flex flex-column justify-center', {
          'justify-center': currentStep !== 'UPLOAD_FILE',
        })}
      >
        {CurrentComponent}
      </div>
    </Modal>
  )
}

const UploadModalWithMutation: React.FC<Props> = props => (
  <SaveRedirectFromFileMutation>
    {(saveRedirectFromFile, mutationResult) => (
      <Mutation<
        DeleteManyRedirectsFromFileResult,
        DeleteManyRedirectsFromFileVariables
      >
        mutation={DeleteManyRedirects}
      >
        {deleteManyRedirects => (
          <UploadModal
            saveRedirectFromFile={saveRedirectFromFile}
            deleteManyRedirects={deleteManyRedirects}
            {...mutationResult}
            {...props}
          />
        )}
      </Mutation>
    )}
  </SaveRedirectFromFileMutation>
)

export default React.memo(UploadModalWithMutation)

```


[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/admin/redirects/UploadModal/typings.d.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
export type ModalStates = 'UPLOAD_FILE' | 'LOADING' | 'ERROR'

```


[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/admin/redirects/UploadModal/UploadError.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { ApolloError } from 'apollo-client'
import { pathOr } from 'ramda'
import React from 'react'
import { FormattedMessage } from 'react-intl'

import DangerIcon from '../../../icons/DangerIcon'
import PaperErrorIcon from '../../../icons/PaperErrorIcon'

import styles from './UploadModal.css'

interface Props {
  error?: ApolloError
}

interface ValidationLineError {
  lineNumber: number
  errors: string[]
}

const UploadError: React.FunctionComponent<Props> = ({ error }) => {
  const message = pathOr<string, string>(
    '',
    ['graphQLErrors', '0', 'message'],
    error || {}
  )
  let errors: ValidationLineError[] = []
  try {
    errors = JSON.parse(message)
  } catch (e) {
    console.error(e)
  }

  return (
    <div className="flex flex-column items-center justify-center">
      <PaperErrorIcon />
      <h2 className="tc fw4 mb3 mt7">
        <FormattedMessage id="admin/pages.admin.redirects.upload-modal.fail.title" />
      </h2>
      <p className="tc c-muted-2 mt2 mb2">
        <FormattedMessage id="admin/pages.admin.redirects.upload-modal.fail.subtitle" />
      </p>
      <p className="tc c-muted-2 mt2">
        <FormattedMessage id="admin/pages.admin.redirects.upload-modal.fail.message" />
      </p>
      <hr
        className={`b--muted-4 br-0 bl-0 bt-0 mh0 mv0 ${styles['full-modal-width']}`}
      />
      <div
        className={`overflow-y-auto ${styles['error-container']} ${styles['full-modal-width']}`}
      >
        {errors.map((errorObj, id) => {
          return (
            <div className="flex pa3 pl7" key={`error-line-${id}`}>
              <div className="mt1">
                <DangerIcon />
              </div>
              <div className="ml4">
                <h2 className="c-danger f5 fw4 mv0">
                  <FormattedMessage id="admin/pages.admin.redirects.upload-modal.error.line.title" />
                  {errorObj.lineNumber}:
                </h2>
                <ul className="f5 fw3 list mt2 pl0">
                  {errorObj.errors.map((errorMessage, messageId) => (
                    <li key={`error-line-${id}-message-${messageId}`}>
                      {errorMessage}
                    </li>
                  ))}
                </ul>
              </div>
            </div>
          )
        })}
      </div>
    </div>
  )
}

export default React.memo(UploadError)

```


[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/admin/redirects/UploadModal/UploadModal.css from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```css
.full-modal-width {
  width: calc(100% + 6rem);
}

.error-container {
  max-height: 30vh;
  margin-bottom: -3rem;
}

.error-container::-webkit-scrollbar,
.error-container::-webkit-scrollbar-track {
  width: 2px;
  background: #f2f4f5;
}

.error-container::-webkit-scrollbar-thumb {
  width: 2px;
  background: rgba(0, 0, 0, 0.2);
  border-radius: 2px;
}

```


[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/admin/redirects/UploadModal/Loading.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { FormattedMessage } from 'react-intl'
import { Button, Progress, Spinner } from 'vtex.styleguide'

interface Props {
  current: number
  total: number
  onCancel: () => void
}

const Loading: React.FunctionComponent<Props> = ({
  current,
  onCancel,
  total,
}) => {
  const percent =
    Math.round((current * 100) / total) < 100
      ? Math.round((current * 100) / total)
      : 100

  return (
    <div className="flex flex-column items-center justify-center">
      <div className="absolute bg-white right-0 top-0 pa5 pa6-ns">
        <Spinner size={30} />
      </div>
      <div className="w-100 flex justify-between mt3 mb4">
        <span>{`${percent}%`}</span>
        <span>
          {current + ' '}/{' ' + total}
        </span>
      </div>
      <Progress type="line" percent={percent} />
      <div className="flex justify-between items-center w-100 mt8">
        <FormattedMessage
          id="admin/pages.admin.redirects.upload-modal.loading.warning"
          defaultMessage="Please don't close this window."
        />
        <Button onClick={onCancel} type="button" variation="secondary">
          <FormattedMessage
            id="admin/pages.admin.redirects.upload-modal.loading.cancel"
            defaultMessage="Cancel"
          />
        </Button>
      </div>
    </div>
  )
}

export default Loading

```


[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/admin/redirects/mutations/DeleteManyRedirectsFromFile.graphql from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```graphql
mutation DeleteManyRedirects($paths: [String!]!, $locators: [RouteLocator!]) {
  redirect @context(provider: "vtex.rewriter") {
    deleteMany(paths: $paths, locators: $locators)
  }
}

```


[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/admin/redirects/mutations/SaveRedirectFromFile.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { Mutation, MutationFn, MutationResult } from 'react-apollo'

import SaveRedirectFromFile from './SaveRedirectFromFile.graphql'

interface SaveRedirectFromFileData {
  saveRedirectFromFile: boolean
}

export type UploadActionType = 'save' | 'delete'

interface SaveRedirectFromFileVariables {
  redirects: Redirect[]
}

type SaveRedirectFromFileMutationFn = MutationFn<
  SaveRedirectFromFileData,
  SaveRedirectFromFileVariables
>

export interface MutationRenderProps
  extends MutationResult<SaveRedirectFromFileData> {
  saveRedirectFromFile: SaveRedirectFromFileMutationFn
}

class SaveRedirectFromFileMutation extends Mutation<
  SaveRedirectFromFileData,
  SaveRedirectFromFileVariables
> {
  public static defaultProps = {
    mutation: SaveRedirectFromFile,
  }
}

export default SaveRedirectFromFileMutation

```


[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/admin/redirects/mutations/DeleteManyRedirectsFromFile.d.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
export interface DeleteManyRedirectsFromFileVariables {
  paths: string[]
}

export type DeleteManyRedirectsFromFileResult = boolean

export type DeleteManyRedirectsFromFileFn = MutationFn<
  DeleteManyRedirectsFromFileVariables,
  DeleteManyRedirectsFromFileResult
>

export interface DeleteManyRedirectsProps {
  deleteManyRedirects: DeleteManyRedirectsFromFileFn
}

```


[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/admin/redirects/mutations/SaveRedirectFromFile.graphql from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```graphql
mutation SaveManyRedirects($redirects: [RedirectInput!]!) {
  redirect @context(provider: "vtex.rewriter") {
    saveMany(routes: $redirects)
  }
}

```


[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/admin/redirects/List/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { Query } from 'react-apollo'
import { Tenant } from 'vtex.tenant-graphql'

import Loader from '../../../Loader'
import List, { Props } from './List'
import TenantInfo from '../../../../queries/TenantInfo.graphql'
import { getStoreBindings } from '../../../../utils/bindings'

const ListWrapper: React.FC<Omit<Props, 'storeBindings'>> = props => {
  return (
    <Query<{ tenantInfo: Tenant }> query={TenantInfo}>
      {({ data, loading: isLoading }) => {
        const tenantInfo = data?.tenantInfo
        if (isLoading || !tenantInfo) {
          return <Loader />
        }
        const storeBindings = getStoreBindings(tenantInfo)
        return <List {...props} storeBindings={storeBindings} />
      }}
    </Query>
  )
}

export default ListWrapper

```


[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/admin/redirects/List/List.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React, { useEffect, useState, useCallback } from 'react'
import { FormattedMessage, IntlShape, useIntl } from 'react-intl'
import { useRuntime } from 'vtex.render-runtime'
import {
  ButtonWithIcon,
  IconUpload,
  Table,
  ToastConsumerFunctions,
} from 'vtex.styleguide'
import { Binding } from 'vtex.tenant-graphql'

import { getFormattedLocalizedDate } from '../../../../utils/date'
import { BASE_URL, NEW_REDIRECT_ID } from '../consts'
import CreateButton from './CreateButton'
import { messages } from './messages'

interface CustomProps {
  from: number
  items: Redirect[]
  loading: boolean
  onHandleDownload: () => void
  onHandleInputSearchChange: (e: React.ChangeEvent<HTMLInputElement>) => void
  openModal: () => void
  refetch: () => void
  showToast: ToastConsumerFunctions['showToast']
  to: number
  storeBindings: Binding[]
}

export type Props = CustomProps

interface Schema {
  properties: object
}

enum CellData {
  TEMPORARY = 'TEMPORARY',
  PERMANENT = 'PERMANENT',
}

const getBindingAddress = (bindingId: string, storeBindings: Binding[]) =>
  storeBindings.find(({ id }) => id === bindingId)?.canonicalBaseAddress

function getSchema(intl: IntlShape, locale: string, storeBindings: Binding[]) {
  const bindingProperty =
    storeBindings.length > 1
      ? {
          binding: {
            title: intl.formatMessage(messages.tableBinding),
            type: 'string',
            cellRenderer: function Binding(cell: { cellData: CellData }) {
              const address = getBindingAddress(cell.cellData, storeBindings)
              return <span className="ph4">{address}</span>
            },
          },
        }
      : {}

  return {
    properties: {
      from: {
        title: intl.formatMessage(messages.tableFrom),
        type: 'string',
      },
      to: {
        title: intl.formatMessage(messages.tableTo),
        type: 'string',
      },
      type: {
        title: intl.formatMessage(messages.tableType),
        type: 'string',
        cellRenderer: function Type(cell: { cellData: CellData }) {
          return cell.cellData && cell.cellData === CellData.TEMPORARY ? (
            <FormattedMessage
              id="admin/pages.admin.redirects.table.type.temporary"
              defaultMessage="Temporary (302)"
            >
              {text => <span className="ph4 silver">{text}</span>}
            </FormattedMessage>
          ) : (
            <FormattedMessage
              id="admin/pages.admin.redirects.table.type.permanent"
              defaultMessage="Permanent (301)"
            >
              {text => <span className="ph4 silver">{text}</span>}
            </FormattedMessage>
          )
        },
      },
      ...bindingProperty,
      endDate: {
        cellRenderer: function EndDate(cell: { cellData: string }) {
          return cell.cellData ? (
            <span className="ph4">
              {getFormattedLocalizedDate(cell.cellData, locale)}
            </span>
          ) : (
            <FormattedMessage
              id="admin/pages.admin.redirects.table.endDate.default"
              defaultMessage="not set"
            >
              {text => <span className="ph4 silver">{text}</span>}
            </FormattedMessage>
          )
        },
        title: intl.formatMessage(messages.endDateTitle),
        type: 'string',
      },
    },
  }
}

const List: React.FC<Props> = ({
  from,
  items,
  loading,
  onHandleDownload,
  onHandleInputSearchChange,
  openModal,
  storeBindings,
}) => {
  const { culture, navigate } = useRuntime()
  const intl = useIntl()

  const [schema, setSchema] = useState<Schema>(() =>
    getSchema(intl, culture.locale, storeBindings)
  )

  useEffect(() => {
    window.top.postMessage({ action: { type: 'STOP_LOADING' } }, '*')
  }, [])

  useEffect(() => {
    setSchema(getSchema(intl, culture.locale, storeBindings))
  }, [culture.locale, from, intl, loading, storeBindings])

  const handleItemView = useCallback(
    (event: { rowData: Redirect }) => {
      const selectedItem = event.rowData
      const bindingQS = selectedItem.from.includes('?')
        ? `&binding=${selectedItem.binding}`
        : `?binding=${selectedItem.binding}`
      navigate({
        to: `${BASE_URL}${selectedItem.from}${bindingQS}`,
      })
    },
    [navigate]
  )

  const handleNewItemOpen = useCallback(() => {
    navigate({ to: `${BASE_URL}/${NEW_REDIRECT_ID}` })
  }, [navigate])

  return (
    <Table
      fullWidth
      loading={loading}
      items={items}
      onRowClick={handleItemView}
      schema={schema}
      emptyStateLabel=""
      emptyStateChildren={
        <div className="pt5 flex flex-column tc">
          <div>
            <CreateButton onClick={handleNewItemOpen} />
          </div>
          <p className="mv2">
            <FormattedMessage
              id="admin/pages.admin.redirects.or.text"
              defaultMessage="or"
            />
          </p>
          <div>
            <ButtonWithIcon
              icon={<IconUpload />}
              variation="secondary"
              onClick={openModal}
              size="small"
            >
              <FormattedMessage
                id="admin/pages.admin.redirects.emptyState.upload"
                defaultMessage="Upload a CSV"
              />
            </ButtonWithIcon>
          </div>
        </div>
      }
      toolbar={{
        inputSearch: {
          onChange: onHandleInputSearchChange,
        },
        density: {
          buttonLabel: intl.formatMessage(messages.lineDensityLabel),
          highOptionLabel: intl.formatMessage(messages.lineDensityHigh),
          lowOptionLabel: intl.formatMessage(messages.lineDensityLow),
          mediumOptionLabel: intl.formatMessage(messages.lineDensityMedium),
        },
        download: {
          disabled: items.length === 0,
          handleCallback: onHandleDownload,
          label: intl.formatMessage(messages.download),
        },
        fields: {
          hideAllLabel: intl.formatMessage(messages.hideAll),
          label: intl.formatMessage(messages.fieldsLabel),
          showAllLabel: intl.formatMessage(messages.showAll),
        },
        newLine: {
          handleCallback: handleNewItemOpen,
          label: intl.formatMessage(messages.newLine),
        },
        upload: {
          handleCallback: openModal,
          label: intl.formatMessage(messages.upload),
        },
      }}
    />
  )
}

export default List

```


[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/admin/redirects/List/typings.d.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

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

export interface FetchMoreOptions {
  updateQuery: (prevData: RedirectsQuery, newData: UpdateQueryNewData) => void
  variables: QueryVariables
}

interface QueryVariables {
  from: number
  to: number
}

interface UpdateQueryNewData {
  fetchMoreResult?: RedirectsQuery
}

```


[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/admin/redirects/List/messages.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { defineMessages } from 'react-intl'

export const messages = defineMessages({
  download: {
    defaultMessage: 'Export',
    id: 'admin/pages.admin.redirects.table.toolbar.export',
  },
  emptyState: {
    defaultMessage: 'No redirects created.',
    id: 'admin/pages.admin.redirects.emptyState',
  },
  endDateTitle: {
    defaultMessage: 'End date',
    id: 'admin/pages.admin.redirects.table.endDate.title',
  },
  fieldsLabel: {
    defaultMessage: 'Toggle visible fields',
    id: 'admin/pages.admin.redirects.table.toolbar.fields.label',
  },
  hideAll: {
    defaultMessage: 'Hide all',
    id: 'admin/pages.admin.redirects.table.toolbar.fields.hide-all',
  },
  lineDensityHigh: {
    defaultMessage: 'High',
    id: 'admin/pages.admin.redirects.table.toolbar.line-density.high',
  },
  lineDensityLabel: {
    defaultMessage: 'Line density',
    id: 'admin/pages.admin.redirects.table.toolbar.line-density.label',
  },
  lineDensityLow: {
    defaultMessage: 'Low',
    id: 'admin/pages.admin.redirects.table.toolbar.line-density.low',
  },
  lineDensityMedium: {
    defaultMessage: 'Medium',
    id: 'admin/pages.admin.redirects.table.toolbar.line-density.medium',
  },
  newLine: {
    defaultMessage: 'New redirect',
    id: 'admin/pages.admin.redirects.button.create',
  },
  showAll: {
    defaultMessage: 'Show all',
    id: 'admin/pages.admin.redirects.table.toolbar.fields.show-all',
  },
  tableFrom: {
    defaultMessage: 'From',
    id: 'admin/pages.admin.redirects.table.from',
  },
  tableTo: {
    defaultMessage: 'To',
    id: 'admin/pages.admin.redirects.table.to',
  },
  tableType: {
    defaultMessage: 'Type',
    id: 'admin/pages.admin.redirects.table.type',
  },
  upload: {
    defaultMessage: 'Import',
    id: 'admin/pages.admin.redirects.table.toolbar.import',
  },
  tableBinding: {
    defaultMessage: 'Binding',
    id: 'admin/pages.admin.redirects.table.binding',
  },
})

```


[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/admin/redirects/List/CreateButton.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { FormattedMessage } from 'react-intl'
import { Button } from 'vtex.styleguide'

interface Props {
  onClick: () => void
}

const CreateButton = ({ onClick }: Props) => (
  <Button onClick={onClick} size="small" variation="primary">
    <FormattedMessage id="admin/pages.admin.redirects.button.create" />
  </Button>
)

export default CreateButton

```


[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/admin/redirects/ImportErrorModal/ImportErrorModal.css from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```css
.import-error-modal-scroll::-webkit-scrollbar,
.import-error-modal-scroll::-webkit-scrollbar-track {
  width: 2px;
  background: #f2f4f5;
}

.import-error-modal-scroll::-webkit-scrollbar-thumb {
  width: 2px;
  background: rgba(0, 0, 0, 0.2);
  border-radius: 2px;
}

```


[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/admin/redirects/ImportErrorModal/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { defineMessages, useIntl, IntlShape } from 'react-intl'
import {
  Button,
  IconDownload,
  Modal,
  ModalProps,
  ButtonWithIcon,
} from 'vtex.styleguide'
import streamSaver from 'streamsaver'
import { TextEncoder } from 'text-encoding'
import * as WebStreamsPolyfill from 'web-streams-polyfill/ponyfill'

import styles from './ImportErrorModal.css'
import { getCSVHeader } from '../consts'
import { AlertState } from '../typings'
import bulkUploadRedirects from '../bulkUploadRedirects'

interface Props extends Omit<ModalProps, 'children'> {
  isOpen: boolean
  redirects: Redirect[]
  mutation: (data: Redirect[]) => Promise<void>
  isSave: boolean
  setAlert: (alertState: AlertState) => void
  hasMultipleBindings: boolean
}

interface GetAlertStateArgs {
  isSave: boolean
  failedRedirects: Redirect[]
  intl: IntlShape
  mutation: (data: Redirect[]) => Promise<void>
  total: number
}

streamSaver.WritableStream = WebStreamsPolyfill.WritableStream

const messages = defineMessages({
  bulkUploadErrorButtonLabel: {
    defaultMessage: 'See errors',
    id: 'admin/pages.admin.redirects.bulk.failed.button',
  },
  buttonCancel: {
    defaultMessage: 'Cancel',
    id: 'admin/pages.admin.redirects.import-error.button.cancel',
  },
  buttonDownload: {
    defaultMessage: 'Download',
    id: 'admin/pages.admin.redirects.import-error.button.download',
  },
  buttonRetry: {
    defaultMessage: 'Retry',
    id: 'admin/pages.admin.redirects.import-error.button.retry',
  },
  retrySuccess: {
    defaultMessage: 'Retry successful.',
    id: 'admin/pages.admin.redirects.import-error.retry-success',
  },
  retryFail: {
    defaultMessage: 'Retry failed: {success} successful, {failed} failed.',
    id: 'admin/pages.admin.redirects.import-error.retry-fail',
  },
  title: {
    defaultMessage: 'Failed Redirects',
    id: 'admin/pages.admin.redirects.import-error.title',
  },
})

function getAlertState({
  isSave,
  total,
  failedRedirects,
  intl,
  mutation,
}: GetAlertStateArgs): AlertState {
  if (failedRedirects.length === 0) {
    return {
      type: 'success',
      message: intl.formatMessage(messages.retrySuccess),
    }
  }
  return {
    type: failedRedirects.length === total ? 'error' : 'warning',
    message: intl.formatMessage(messages.retryFail, {
      success: intl.formatNumber(total - failedRedirects.length),
      failed: intl.formatNumber(failedRedirects.length),
    }),
    meta: {
      failedRedirects: failedRedirects,
      mutation,
      isSave,
    },
    action: {
      label: intl.formatMessage(messages.bulkUploadErrorButtonLabel),
    },
  }
}

const ImportErrorModal: React.FC<Props> = ({
  isSave,
  redirects,
  isOpen,
  mutation,
  onClose,
  setAlert,
  hasMultipleBindings,
}) => {
  const intl = useIntl()
  const shouldUploadRef = React.useRef<boolean>(false)

  React.useEffect(() => {
    shouldUploadRef.current = isOpen
  }, [isOpen])

  const [isRetrying, setIsRetrying] = React.useState(false)

  const handleDownload = React.useCallback(() => {
    const fileStream = streamSaver.createWriteStream('failed_redirects.csv')
    const writer = fileStream.getWriter()
    const textEncoder = new TextEncoder()
    writer.write(textEncoder.encode(getCSVHeader(hasMultipleBindings) + '\n'))
    redirects.forEach(({ endDate, from, to, type }) => {
      writer.write(
        textEncoder.encode(`${from};${to};${type};${endDate || ''}\n`)
      )
    })
    writer.close()
  }, [redirects, hasMultipleBindings])

  const handleRetry = React.useCallback(async () => {
    shouldUploadRef.current = true
    try {
      const numberOfLines = redirects.length

      setIsRetrying(true)

      const { failedRedirects } = await bulkUploadRedirects({
        data: redirects,
        isSave,
        mutation,
        shouldUploadRef,
      })

      setIsRetrying(false)

      if (shouldUploadRef.current) {
        setAlert(
          getAlertState({
            total: numberOfLines,
            failedRedirects,
            intl,
            mutation,
            isSave,
          })
        )
        onClose()
      }
    } catch (e) {
      console.error(e)
    }
  }, [redirects, isSave, mutation, setAlert, intl, onClose])

  return (
    <Modal
      centered
      isOpen={isOpen}
      onClose={onClose}
      title={<h2 className="mv0">{intl.formatMessage(messages.title)}</h2>}
      bottomBar={
        <>
          <Button
            variation="secondary"
            isLoading={isRetrying}
            onClick={handleRetry}
          >
            {intl.formatMessage(messages.buttonRetry)}
          </Button>
          <span className="mr4">
            <Button
              disabled={isRetrying}
              variation="tertiary"
              onClick={onClose}
            >
              {intl.formatMessage(messages.buttonCancel)}
            </Button>
          </span>
          <span className="mr-auto">
            <ButtonWithIcon
              disabled={isRetrying}
              icon={<IconDownload />}
              variation="tertiary"
              onClick={handleDownload}
            >
              {intl.formatMessage(messages.buttonDownload)}
            </ButtonWithIcon>
          </span>
        </>
      }
    >
      <ul
        className={`list pl0 b--muted-5 ba br2 f6 lh-copy pa3 ${styles['import-error-modal-scroll']}`}
      >
        <li>{getCSVHeader(hasMultipleBindings)}</li>
        {redirects.map(({ endDate, from, to, type }) => (
          <li
            key={`failed-redirect-${from}`}
          >{`${from};${to};${type};${endDate || ''}`}</li>
        ))}
      </ul>
    </Modal>
  )
}

export default React.memo(ImportErrorModal)

```


[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/admin/redirects/UploadModal/UploadPrompt/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import classnames from 'classnames'
import { zipObj } from 'ramda'
import React, { useState, useCallback } from 'react'
import { useDropzone } from 'react-dropzone'
import { defineMessages, useIntl } from 'react-intl'
import { Alert, AlertProps, Button, IconDelete, Radio } from 'vtex.styleguide'

import UploadTableIcon from '../../../../icons/UploadTable'
import { UploadActionType } from '../../mutations/SaveRedirectFromFile'
import { getCSVHeader, CSV_SEPARATOR } from '../../consts'
import { isRedirectType, validateRedirect } from './validateRedirect'
import styles from './UploadPrompt.css'

interface Props {
  hasRedirects: boolean
  onCancel: () => void
  onDownloadTemplate: () => void
  saveRedirectFromFile: (
    parsedJsonFromCsv: Redirect[],
    fileName: string,
    hasMultipleBindings: boolean
  ) => void
  setUploadActionType: React.Dispatch<React.SetStateAction<UploadActionType>>
  uploadActionType: UploadActionType
  hasMultipleBindings: boolean
}

const messages = defineMessages({
  alertSeeErrors: {
    defaultMessage: 'See errors',
    id:
      'admin/pages.admin.redirects.upload-modal.prompt.alert.validation-errors.action',
  },
  body: {
    defaultMessage: 'Please use our template or an exported file as base.',
    id: 'admin/pages.admin.redirects.upload-modal.prompt.body',
  },
  buttonCancel: {
    defaultMessage: 'Cancel',
    id: 'admin/pages.admin.redirects.upload-modal.prompt.button.cancel',
  },
  buttonImport: {
    defaultMessage: 'Import file',
    id: 'admin/pages.admin.redirects.upload-modal.prompt.button.import',
  },
  dropButton: {
    defaultMessage: 'choose a file',
    id: 'admin/pages.admin.redirects.upload-modal.prompt.drop.button',
  },
  dropMessage: {
    defaultMessage: 'Drop here your CSV or ',
    id: 'admin/pages.admin.redirects.upload-modal.prompt.drop.message',
  },
  downloadTemplate: {
    defaultMessage: 'Download template',
    id:
      'admin/pages.admin.redirects.upload-modal.prompt.body.download-template',
  },
  selectSave: {
    defaultMessage: 'Save',
    id: 'admin/pages.admin.redirects.upload-modal.prompt.select-action.save',
  },
  selectDelete: {
    defaultMessage: 'Delete',
    id: 'admin/pages.admin.redirects.upload-modal.prompt.select-action.delete',
  },
  selectUploadBody: {
    defaultMessage: 'Do you want to save or delete redirects?',
    id: 'admin/pages.admin.redirects.upload-modal.prompt.select-action.body',
  },
  unsupportedFileFormat: {
    defaultMessage: 'Unsupported file type. Please upload a .csv file.',
    id:
      'admin/pages.admin.redirects.upload-modal.prompt.alert.unsupported-file',
  },
  validationErrors: {
    defaultMessage: 'Validation errors on file {name}',
    id:
      'admin/pages.admin.redirects.upload-modal.prompt.alert.validation-errors',
  },
  missingBindingData: {
    defaultMessage: 'Binding information missing from redirect {route}',
    id:
      'admin/pages.admin.redirects.upload-modal.prompt.alert.missing-binding-data',
  },
  extraBindingData: {
    defaultMessage: 'Uneccessary binding information from redirect {route}',
    id:
      'admin/pages.admin.redirects.upload-modal.prompt.alert.extra-binding-data',
  },
})

interface AlertState {
  message: string
  action?: AlertProps['action']
}

const UploadPrompt: React.FC<Props> = ({
  hasRedirects,
  onCancel,
  onDownloadTemplate,
  saveRedirectFromFile,
  setUploadActionType,
  uploadActionType,
  hasMultipleBindings,
}) => {
  const intl = useIntl()
  const [file, setFile] = useState<File | null>(null)
  const [shouldShowErrors, setShouldShowError] = useState(false)
  const [parsedJsonFromCsv, setParsedJsonFromCsv] = useState<Redirect[] | null>(
    null
  )

  const [alertState, setAlert] = useState<AlertState | null>(null)
  const [validationErrors, setValidationErrors] = useState<string[] | null>(
    null
  )

  const resetErrors = useCallback(() => {
    setShouldShowError(false)
    setValidationErrors(null)
    setAlert(null)
  }, [setShouldShowError, setValidationErrors, setAlert])

  const onDrop = useCallback(
    acceptedFiles => {
      const file = acceptedFiles[0]
      const fileExtension = file.name.substr(file.name.lastIndexOf('.') + 1)
      let nextValidationErrors: typeof validationErrors = null

      if (fileExtension !== 'csv') {
        return setAlert({
          message: intl.formatMessage(messages.unsupportedFileFormat),
        })
      }

      resetErrors()

      const reader = new FileReader()
      reader.onload = async () => {
        const redirectKeys = getCSVHeader(hasMultipleBindings).split(
          CSV_SEPARATOR
        ) as Array<keyof Redirect>
        let parsedJsonFromCsv = null
        if (typeof reader.result === 'string') {
          parsedJsonFromCsv = reader.result
            .trim()
            .split('\n')
            .reduce<Redirect[]>((acc, line, lineNumber) => {
              if (lineNumber === 0) {
                return acc
              }
              const values = line.split(CSV_SEPARATOR)
              const redirect = zipObj(redirectKeys, values)
              const wrongNumberOfPropeties =
                redirectKeys.length != values.length
              if (hasMultipleBindings && wrongNumberOfPropeties) {
                nextValidationErrors = (nextValidationErrors || []).concat(
                  intl.formatMessage(messages.missingBindingData, {
                    route: redirect.from,
                  })
                )
                return acc
              }
              if (!hasMultipleBindings && wrongNumberOfPropeties) {
                nextValidationErrors = (nextValidationErrors || []).concat(
                  intl.formatMessage(messages.extraBindingData, {
                    route: redirect.from,
                  })
                )
                return acc
              }
              if (isRedirectType(redirect)) {
                const validationError = validateRedirect(
                  redirect,
                  lineNumber,
                  intl
                )
                if (!validationError) {
                  acc.push({
                    ...redirect,
                    endDate: redirect.endDate || null,
                    type: redirect.type.toUpperCase() as Redirect['type'],
                  })
                } else {
                  nextValidationErrors = (nextValidationErrors || []).concat(
                    validationError
                  )
                }
              }
              return acc
            }, [])
        }

        if (nextValidationErrors && nextValidationErrors.length > 0) {
          setAlert({
            message: intl.formatMessage(messages.validationErrors, {
              name: file.name,
            }),
            action: {
              label: intl.formatMessage(messages.alertSeeErrors),
              onClick: () => setShouldShowError(value => !value),
            },
          })
          return setValidationErrors(nextValidationErrors)
        }

        setParsedJsonFromCsv(parsedJsonFromCsv)
      }
      reader.readAsText(file)
      setFile(file)
    },
    [intl, resetErrors, validationErrors, hasMultipleBindings]
  )

  const { getRootProps, getInputProps } = useDropzone({
    onDrop,
    multiple: false,
  })

  const handleSaveRadioChange = useCallback(() => {
    setUploadActionType('save')
  }, [setUploadActionType])

  const handleDeleteRadioChange = useCallback(() => {
    setUploadActionType('delete')
  }, [setUploadActionType])

  return (
    <>
      <p className="f4 fw3 mt0 mb5">
        {intl.formatMessage(messages.selectUploadBody)}
      </p>
      <Radio
        disabled={!hasRedirects}
        id="merge-upload-action-type"
        checked={uploadActionType === 'save'}
        label={intl.formatMessage(messages.selectSave)}
        name="upload-action-type"
        onChange={handleSaveRadioChange}
        value="save"
      />
      <Radio
        disabled={!hasRedirects}
        id="overwrite-upload-action-type"
        checked={uploadActionType === 'delete'}
        label={intl.formatMessage(messages.selectDelete)}
        name="upload-action-type"
        onChange={handleDeleteRadioChange}
        value="delete"
      />
      <p className="f5 lh-copy mv5">{intl.formatMessage(messages.body)}</p>

      {alertState && (
        <>
          <Alert type="error" action={alertState.action}>
            {alertState.message}
          </Alert>
          {shouldShowErrors && validationErrors && validationErrors.length > 0 && (
            <ul
              className={`list pl0 b--muted-5 ba br2 f6 lh-copy overflow-y-auto pa3 ${styles['validation-errors-container']}`}
            >
              {validationErrors.map((errorMessage, id) => (
                <li key={`validation-error-${id}`}>{errorMessage}</li>
              ))}
            </ul>
          )}
        </>
      )}

      <div
        className={classnames(
          'w-100 ba br3 bw1 b--dashed flex items-center mt4',
          {
            'b--action-secondary hover-b--action-secondary bg-white justify-center': !file,
            'b--muted-3 hover-b--muted-2 bg-muted-5 justify-between ph5': !!file,
          }
        )}
      >
        {!file ? (
          <div
            {...getRootProps({
              className:
                'flex flex-column items-center justify-center w-100 pv8',
            })}
          >
            <input {...getInputProps()} />
            <div className="c-action-primary">
              <UploadTableIcon />
            </div>
            <p className="tc lh-copy mb0">
              {intl.formatMessage(messages.dropMessage)}{' '}
              <button className="input-reset bg-white bw0 fw5 pa0 hover-c-action-primary c-action-primary pointer">
                {intl.formatMessage(messages.dropButton)}
              </button>
              .
            </p>
          </div>
        ) : (
          <>
            <p className="c-on-base">{file.name}</p>
            <button
              className="input-reset pa0 bn c-action-primary pointer bg-transparent"
              onClick={() => {
                setFile(null)
                resetErrors()
              }}
            >
              <IconDelete />
            </button>
          </>
        )}
      </div>

      <div className="flex mt5">
        <div className="mr-auto">
          <Button
            type="button"
            variation="tertiary"
            onClick={onDownloadTemplate}
          >
            {intl.formatMessage(messages.downloadTemplate)}
          </Button>
        </div>
        <div className="mr3">
          <Button type="button" variation="tertiary" onClick={onCancel}>
            {intl.formatMessage(messages.buttonCancel)}
          </Button>
        </div>
        <Button
          type="button"
          variation="primary"
          disabled={!parsedJsonFromCsv || validationErrors}
          onClick={() =>
            parsedJsonFromCsv &&
            file &&
            saveRedirectFromFile(
              parsedJsonFromCsv,
              file.name,
              hasMultipleBindings
            )
          }
        >
          {intl.formatMessage(messages.buttonImport)}
        </Button>
      </div>
    </>
  )
}

export default React.memo(UploadPrompt)

```


[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/admin/redirects/UploadModal/UploadPrompt/validateRedirect.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { defineMessages, IntlShape } from 'react-intl'

export function isRedirectType(obj: unknown): obj is Redirect {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'endDate' in obj &&
    'from' in obj &&
    'to' in obj &&
    'type' in obj &&
    typeof (obj as Record<string, string>).type === 'string'
  )
}

const messages = defineMessages({
  invalidTypeError: {
    defaultMessage:
      '[Line {line}] Invalid value "{type}" for redirect type. Allowed values are "PERMANENT" and "TEMPORARY".',
    id: 'admin/pages.admin.redirects.upload-modal.prompt.validation-error.type',
  },
})

export function validateRedirect(
  redirect: Redirect,
  line: number,
  intl: IntlShape
): string | undefined {
  const { type } = redirect
  return type.toUpperCase() !== 'PERMANENT' &&
    type.toUpperCase() !== 'TEMPORARY'
    ? intl.formatMessage(messages.invalidTypeError, {
        line,
        type,
      })
    : undefined
}

```


[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/admin/redirects/UploadModal/UploadPrompt/UploadPrompt.css from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```css
.validation-errors-container {
  max-height: 16rem;
}

.validation-errors-container::-webkit-scrollbar,
.validation-errors-container::-webkit-scrollbar-track {
  width: 4px;
  background: #f2f4f5;
}

.validation-errors-container::-webkit-scrollbar-thumb {
  width: 4px;
  background: rgba(0, 0, 0, 0.2);
  border-radius: 2px;
}

```


[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/admin/redirects/UploadModal/UploadPrompt/getAlertState.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { defineMessages, IntlShape } from 'react-intl'

import { AlertState } from '../../typings'

interface GetAlertStateArgs {
  isSave: boolean
  failedRedirects: Redirect[]
  fileName: string
  intl: IntlShape
  mutation: (data: Redirect[]) => Promise<void>
  total: number
}

const messages = defineMessages({
  bulkUploadAllFailed: {
    defaultMessage: `We couldn't process all {lines} lines from file {fileName}. Please try again.`,
    id: 'admin/pages.admin.redirects.bulk.failed',
  },
  bulkUploadErrorButtonLabel: {
    defaultMessage: 'See errors',
    id: 'admin/pages.admin.redirects.bulk.failed.button',
  },
  bulkUploadPartial: {
    defaultMessage:
      'Import of file {fileName} was partially successful: {success} successful, {failed} failed.',
    id: 'admin/pages.admin.redirects.bulk.partial',
  },
  bulkUploadSuccess: {
    defaultMessage:
      'All done. {lines} lines were successfully processed from file "{fileName}"',
    id: 'admin/pages.admin.redirects.bulk.success',
  },
})

export function getAlertState({
  isSave,
  total,
  failedRedirects,
  intl,
  fileName,
  mutation,
}: GetAlertStateArgs): AlertState {
  if (failedRedirects.length > 0 && failedRedirects.length === total) {
    return {
      type: 'error',
      message: intl.formatMessage(messages.bulkUploadAllFailed, {
        fileName,
        lines: intl.formatNumber(total),
      }),
    }
  } else if (failedRedirects.length === 0) {
    return {
      type: 'success',
      message: intl.formatMessage(messages.bulkUploadSuccess, {
        fileName,
        lines: intl.formatNumber(total),
      }),
    }
  }
  return {
    type: 'warning',
    message: intl.formatMessage(messages.bulkUploadPartial, {
      fileName,
      success: intl.formatNumber(total - failedRedirects.length),
      failed: intl.formatNumber(failedRedirects.length),
    }),
    meta: {
      failedRedirects: failedRedirects,
      mutation,
      isSave,
    },
    action: {
      label: intl.formatMessage(messages.bulkUploadErrorButtonLabel),
    },
  }
}

```


[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/admin/pages/Form/index.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { diff } from 'deep-object-diff'
import { RouteFormData } from 'pages'
import { isEmpty } from 'ramda'
import React, { Component } from 'react'
import { defineMessages, InjectedIntlProps, injectIntl } from 'react-intl'
import { withRuntimeContext } from 'vtex.render-runtime'
import {
  ConditionsProps,
  ConditionsStatement,
  ToastConsumerFunctions,
} from 'vtex.styleguide'
import { Binding } from 'vtex.tenant-graphql'

import { formatStatements } from '../../../../utils/conditions'
import { isNewRoute, isUserRoute } from '../utils'
import { generateNewRouteId } from './utils'
import Form from './Form'
import {
  getAddConditionalTemplateState,
  getChangeOperatorConditionalTemplateState,
  getChangeStatementsConditionalTemplate,
  getChangeTemplateConditionalTemplateState,
  getLoginToggleState,
  getRemoveConditionalTemplateState,
  getValidateFormState,
} from './stateHandlers'
import { FormErrors } from './typings'
import { OperationsResults } from './Operations'
import { slugify } from '../../utils'
import { DropdownChangeInput } from '../../../../utils/bindings/typings'

interface ComponentProps {
  isCustomPage: boolean
  initialData: RouteFormData
  onDelete: OperationsResults['deleteRoute']
  onExit: () => void
  onSave: OperationsResults['saveRoute']
  runtime: RenderContext
  templates: Template[]
  showToast: ToastConsumerFunctions['showToast']
  hideToast: ToastConsumerFunctions['hideToast']
  storeBindings: Binding[]
  setLocalStorageBinding: (b: Binding) => void
}

type Props = ComponentProps & InjectedIntlProps

export interface State {
  data: RouteFormData
  isDeletable: boolean
  isLoading: boolean
  isInfoEditable: boolean
  formErrors: FormErrors
  storeBindings: Binding[]
}

const messages = defineMessages({
  deleteError: {
    defaultMessage: 'Error: route could not be deleted.',
    id: 'admin/pages.admin.pages.form.delete.error',
  },
  deleteSuccess: {
    defaultMessage: 'Success',
    id: 'admin/pages.admin.pages.form.delete.success',
  },
  saveError: {
    defaultMessage: 'Error: route could not be saved.',
    id: 'admin/pages.admin.pages.form.save.error',
  },
  saveSuccess: {
    defaultMessage: 'Success',
    id: 'admin/pages.admin.pages.form.save.success',
  },
})

class FormContainer extends Component<Props, State> {
  private isNew: boolean

  public constructor(props: Props) {
    super(props)

    const { declarer } = props.initialData || { declarer: null }

    this.isNew = isNewRoute(props.initialData)
    this.state = {
      data: props.initialData,
      formErrors: {},
      isDeletable: !declarer && !this.isNew,
      isInfoEditable: !declarer || this.isNew,
      isLoading: false,
      storeBindings: props.storeBindings,
    }
  }

  public componentDidMount() {
    window.top.postMessage({ action: { type: 'STOP_LOADING' } }, '*')
  }

  public render() {
    const { isCustomPage, templates, onExit } = this.props
    const {
      data,
      formErrors,
      isDeletable,
      isInfoEditable,
      isLoading,
      storeBindings,
    } = this.state

    return (
      <Form
        isCustomPage={isCustomPage}
        data={data}
        detailChangeHandlerGetter={this.getDetailChangeHandler}
        isDeletable={isDeletable}
        isLoading={isLoading}
        isInfoEditable={isInfoEditable}
        onDelete={this.handleDelete}
        onExit={onExit}
        onLoginToggle={this.handleLoginToggle}
        onSave={this.handleSave}
        templates={templates}
        storeBindings={storeBindings}
        onAddConditionalTemplate={this.handleAddConditionalTemplate}
        onChangeKeywords={this.handleMetaTagKeywords}
        onRemoveConditionalTemplate={this.handleRemoveConditionalTemplate}
        onChangeTemplateConditionalTemplate={
          this.handleChangeTemplateConditionalTemplate
        }
        onChangeBinding={this.handleBindingChange}
        onChangeOperatorConditionalTemplate={
          this.handleChangeOperatorConditionalTemplate
        }
        onChangeStatementsConditionalTemplate={
          this.handleChangeStatementsConditionalTemplate
        }
        formErrors={formErrors}
      />
    )
  }

  private handleRemoveConditionalTemplate = (uniqueId: number) => {
    this.setState(getRemoveConditionalTemplateState(uniqueId))
  }

  private handleAddConditionalTemplate = () => {
    this.setState(getAddConditionalTemplateState)
  }

  private handleBindingChange = ({
    target: { value },
  }: DropdownChangeInput) => {
    const { setLocalStorageBinding } = this.props
    const { storeBindings, data } = this.state
    const selectedBinding = storeBindings.find(binding => binding.id === value)
    if (!selectedBinding) {
      throw new Error(
        "Selected binding does not exist or isn't a store binding"
      )
    }
    data.binding = selectedBinding.id
    setLocalStorageBinding(selectedBinding)
    this.setState({ data })
  }

  private handleChangeTemplateConditionalTemplate = (
    uniqueId: number,
    template: string
  ) => {
    this.setState(getChangeTemplateConditionalTemplateState(uniqueId, template))
  }

  private handleChangeOperatorConditionalTemplate = (
    uniqueId: number,
    operator: NonNullable<ConditionsProps['operator']>
  ) => {
    this.setState(getChangeOperatorConditionalTemplateState(uniqueId, operator))
  }

  private handleChangeStatementsConditionalTemplate = (
    uniqueId: number,
    statements: ConditionsStatement[]
  ) => {
    this.setState(getChangeStatementsConditionalTemplate(uniqueId, statements))
  }

  private handleMetaTagKeywords = (
    values: { label: string; value: string }[]
  ) => {
    this.setState(prevState => ({
      ...prevState,
      data: {
        ...prevState.data,
        metaTagKeywords: values,
      },
      formErrors: {},
    }))
  }

  private getDetailChangeHandler = (detailName: string) => (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    const newDetailValue = e.target.value
    const newDataPayload = {
      [detailName]: newDetailValue,
      ...(detailName === 'title' &&
        this.isNew && { path: `/${slugify(newDetailValue)}` }),
    }

    this.setState(prevState => ({
      ...prevState,
      data: {
        ...prevState.data,
        ...newDataPayload,
      },
      formErrors: {},
    }))
  }

  private handleDelete = () => {
    const { showToast, intl, onDelete, onExit } = this.props
    const { data } = this.state

    this.setState({ isLoading: true }, async () => {
      try {
        if (!data.uuid) {
          throw new Error('No uuid')
        }

        await onDelete({
          variables: {
            uuid: data.uuid,
          },
        })

        showToast({
          horizontalPosition: 'right',
          message: intl.formatMessage(messages.deleteSuccess),
        })

        onExit()
      } catch (err) {
        this.setState({ isLoading: false }, () => {
          console.error(err)

          showToast({
            horizontalPosition: 'right',
            message: intl.formatMessage(messages.deleteError),
          })
        })
      }
    })
  }

  private handleLoginToggle = () => {
    this.setState(getLoginToggleState)
  }

  private handleSave = (event: React.FormEvent) => {
    const { intl, onExit, onSave, showToast } = this.props

    event.preventDefault()

    const nextState = getValidateFormState(this.state)

    if (isEmpty(nextState.formErrors)) {
      const {
        auth,
        binding: bindingId,
        blockId,
        context,
        declarer,
        domain,
        interfaceId,
        metaTagDescription,
        metaTagKeywords,
        metaTagRobots,
        pages,
        path,
        routeId,
        title,
        uuid,
        dataSource,
      } = isUserRoute(this.props.initialData)
        ? this.state.data
        : {
            ...(diff(this.props.initialData, this.state.data) as Partial<
              RouteFormData
            >),
            ...this.state.data,
            pages: this.state.data.pages || this.state.data,
            blockId: this.state.data.blockId || this.props.initialData,
          }

      const metaTags =
        metaTagDescription || metaTagKeywords || metaTagRobots
          ? {
              description: metaTagDescription,
              keywords: metaTagKeywords?.map(({ value }) => value),
              robots: metaTagRobots
            }
          : undefined

      this.setState({ isLoading: true }, async () => {
        try {
          await onSave({
            variables: {
              route: {
                auth,
                bindingId,
                blockId,
                context,
                declarer,
                domain,
                interfaceId,
                metaTags,
                pages:
                  pages &&
                  pages.map(page => {
                    return {
                      condition: {
                        allMatches: page.condition.allMatches,
                        id: page.condition.id || undefined,
                        statements: formatStatements(page.condition.statements),
                      },
                      pageId: page.pageId || undefined,
                      template: page.template,
                    }
                  }),
                path,
                routeId:
                  routeId ||
                  generateNewRouteId(
                    interfaceId || this.state.data.interfaceId,
                    path
                  ),
                title,
                uuid,
                dataSource,
              },
            },
          })

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

          onExit()
        } catch (err) {
          this.setState({ isLoading: false }, () => {
            console.error(err)

            showToast({
              horizontalPosition: 'right',
              message: intl.formatMessage(messages.saveError),
            })
          })
        }
      })
    } else {
      this.setState(getValidateFormState)
    }
  }
}

export default withRuntimeContext(injectIntl(FormContainer))

```


[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/admin/pages/Form/Title.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'

const Title: React.FunctionComponent = ({ children }) => (
  <h1 className="mb7 pb7 bb bw1 b--light-silver f3 normal">{children}</h1>
)

export default Title

```


[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/admin/pages/Form/ConditionalTemplatePicker.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import { path } from 'ramda'
import React from 'react'
import {
  defineMessages,
  WrappedComponentProps as ComponentWithIntlProps,
} from 'react-intl'
import {
  ConditionsProps,
  ConditionsStatement,
  Dropdown,
  IconClose,
} from 'vtex.styleguide'
import { ConditionFormsData } from 'pages'

import SelectConditions from './SelectConditions'
import { FormErrors } from './typings'

export interface ConditionalTemplatePickerProps {
  condition: ConditionFormsData
  formErrors: FormErrors
  onChangeOperatorConditionalTemplate: (
    uniqueId: number,
    operator: NonNullable<ConditionsProps['operator']>
  ) => void
  onChangeTemplateConditionalTemplate: (
    uniqueId: number,
    template: string
  ) => void
  onChangeStatementsConditionalTemplate: (
    uniqueId: number,
    statements: ConditionsStatement[]
  ) => void
  onRemoveConditionalTemplate: (uniqueId: number) => void
  operator: ConditionsProps['operator']
  pageId: number
  template: string
  templates: Template[]
}

type Props = ConditionalTemplatePickerProps & ComponentWithIntlProps

const messages = defineMessages({
  errorMessage: {
    defaultMessage: 'Required',
    id: 'admin/pages.admin.pages.form.templates.field.required',
  },
  pathValidationErrorMessage: {
    defaultMessage: `URL should begin with '/'.`,
    id: 'admin/pages.admin.pages.form.templates.path.validation-error',
  },
  templateLabel: {
    defaultMessage: 'Template',
    id: 'admin/pages.admin.pages.form.templates.conditional.template.label',
  },
})

export const ConditionalTemplatePicker: React.FunctionComponent<Props> = ({
  condition,
  formErrors,
  intl,
  onChangeOperatorConditionalTemplate,
  onChangeStatementsConditionalTemplate,
  onChangeTemplateConditionalTemplate,
  operator,
  onRemoveConditionalTemplate,
  pageId,
  template,
  templates,
}) => {
  const hasError =
    !!path(['pages', pageId, 'condition'], formErrors) ||
    !!path(['pages', pageId, 'template'], formErrors)

  const closeButtonStyle = hasError
    ? {
        marginBottom: 'auto',
        marginTop: 'auto',
      }
    : undefined

  return (
    <div className={`flex ${!hasError ? 'items-center' : ''} mv5`}>
      <div className="flex flex-column flex-grow-1 mr4 ph4">
        <Dropdown
          label={intl.formatMessage(messages.templateLabel)}
          options={templates.map(({ id }) => ({ value: id, label: id }))}
          onChange={(_: React.ChangeEvent, value: string) =>
            onChangeTemplateConditionalTemplate(pageId, value)
          }
          value={template}
          errorMessage={
            path(['pages', pageId, 'template'], formErrors) &&
            intl.formatMessage({
              id: path(['pages', pageId, 'template'], formErrors) as string,
            })
          }
        />
        <div className="flex flex-column w-100 mt5">
          <SelectConditions
            onChangeOperator={({ operator: newOperator }) => {
              onChangeOperatorConditionalTemplate(pageId, newOperator)
            }}
            operator={operator}
            condition={condition}
            onChangeStatements={statements => {
              onChangeStatementsConditionalTemplate(pageId, statements)
            }}
            errorMessage={
              path(['pages', pageId, 'condition'], formErrors) &&
              intl.formatMessage({
                id: path(['pages', pageId, 'condition'], formErrors) as string,
              })
            }
          />
        </div>
      </div>
      <button
        type="button"
        className="w1 h1 bg-silver br-100 flex items-center justify-center bn pa1 mt6"
        style={closeButtonStyle}
        onClick={() => onRemoveConditionalTemplate(pageId)}
      >
        <IconClose color="white" size={12} />
      </button>
    </div>
  )
}

```


[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/admin/pages/Form/utils.test.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

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

describe('#generateNewRouteId', () => {
  it('should remove trailing slash from path', () => {
    expect(
      generateNewRouteId('vtex.store@2.x:store.custom', '/path1/path2/')
    ).toBe('vtex.store@2.x:store.custom#path1-path2')
  })

  it('should generate a routeId without "/"', () => {
    expect(
      generateNewRouteId('vtex.store@2.x:store.custom', '/path1/path2')
    ).toEqual(expect.not.stringContaining('/'))
  })
})

```


[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/admin/pages/Form/SelectConditions.tsx from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import {
  FormattedMessage,
  injectIntl,
  WrappedComponentProps as ComponentWithIntlProps,
} from 'react-intl'
import { withRuntimeContext } from 'vtex.render-runtime'
import {
  ConditionsProps,
  ConditionsRenderFn,
  ConditionsStatement,
  DatePicker,
  EXPERIMENTAL_Conditions as Conditions,
} from 'vtex.styleguide'
import { ConditionFormsData } from 'pages'

import { messages } from './messages'

interface ComponentProps {
  condition: ConditionFormsData
  errorMessage?: string
  operator: ConditionsProps['operator']
  onChangeOperator: ConditionsProps['onChangeOperator']
  onChangeStatements: (statements: ConditionsStatement[]) => void
}

type Props = ComponentProps & RenderContextProps & ComponentWithIntlProps

class SelectConditions extends React.Component<Props> {
  private options: ConditionsProps['options']
  private labels: ConditionsProps['labels']

  public constructor(props: Props) {
    super(props)

    const intl = props.intl

    this.labels = {
      addConditionBtn: intl.formatMessage(messages.addCondition),
      addNewCondition: intl.formatMessage(messages.addCondition),
      delete: intl.formatMessage(messages.delete),
      headerPrefix: intl.formatMessage(messages.headerPrefix),
      headerSufix: intl.formatMessage(messages.headerSuffix),
      noConditions: intl.formatMessage(messages.noConditions),
      operatorAll: intl.formatMessage(messages.operatorAll),
      operatorAnd: intl.formatMessage(messages.operatorAnd),
      operatorAny: intl.formatMessage(messages.operatorAny),
      operatorOr: intl.formatMessage(messages.operatorOr),
    }

    this.options = {
      date: {
        label: intl.formatMessage(messages.dateLabel),
        unique: true,
        verbs: [
          {
            label: intl.formatMessage(messages.dateEquals),
            object: {
              extraParams: {},
              renderFn: this.complexDatePickerObject,
            },
            value: 'is',
          },
          {
            label: intl.formatMessage(messages.dateRange),
            object: {
              extraParams: {},
              renderFn: this.complexDatePickerRangeObject,
            },
            value: 'between',
          },
          {
            label: intl.formatMessage(messages.dateTo),
            object: {
              extraParams: {},
              renderFn: this.complexDatePickerObject,
            },
            value: 'to',
          },
          {
            label: intl.formatMessage(messages.dateFrom),
            object: {
              extraParams: {},
              renderFn: this.complexDatePickerObject,
            },
            value: 'from',
          },
        ],
      },
    }
  }

  public render() {
    return (
      <>
        <Conditions
          labels={this.labels}
          options={this.options}
          subjectPlaceholder={this.props.intl.formatMessage(
            messages.subjectPlaceholder
          )}
          noOptionsMessage={this.props.intl.formatMessage(
            messages.emptyConditionsOptions
          )}
          statements={this.props.condition.statements}
          operator={this.props.operator}
          onChangeOperator={this.handleToggleOperator}
          onChangeStatements={statements => {
            this.props.onChangeStatements(
              statements.map(statement => ({
                ...statement,
                objectJSON: JSON.stringify(statement.object || null),
              }))
            )
          }}
        />
        <span className="c-danger t-small mt3 lh-title">
          {this.props.errorMessage}
        </span>
      </>
    )
  }

  private handleToggleOperator: ConditionsProps['onChangeOperator'] = operatorObj => {
    if (typeof this.props.onChangeOperator === 'function') {
      this.props.onChangeOperator(operatorObj)
    }
  }

  private complexDatePickerObject: ConditionsRenderFn = ({
    statements,
    values,
    statementIndex,
  }) => {
    const {
      culture: { locale },
    } = this.props.runtime
    return (
      <DatePicker
        value={values && values.date}
        onChange={(date: Date) => {
          const newStatements = statements.map((statement, index) => {
            const dateAtMidnight = new Date(date)
            dateAtMidnight.setHours(0, 0, 0, 0)

            return {
              ...statement,
              object:
                index === statementIndex
                  ? { date: dateAtMidnight }
                  : statement.object,
            }
          })
          this.props.onChangeStatements(newStatements)
        }}
        locale={locale}
      />
    )
  }

  private complexDatePickerRangeObject: ConditionsRenderFn = ({
    statements,
    values,
    statementIndex,
  }) => {
    const {
      culture: { locale },
    } = this.props.runtime
    return (
      <div className="flex">
        <div style={{ maxWidth: 140 }}>
          <DatePicker
            value={values && values.from}
            onChange={(date: Date) => {
              const newStatements = statements.map((statement, index) => {
                return {
                  ...statement,
                  object: {
                    ...statement.object,
                    ...(index === statementIndex ? { from: date } : null),
                  },
                }
              })
              this.props.onChangeStatements(newStatements)
            }}
            locale={locale}
          />
        </div>

        <FormattedMessage
          id="admin/pages.admin.pages.form.templates.simple.conditions.date.range.and"
          defaultMessage="and"
        >
          {message => <div className="mv4 mh3 c-muted-2 b">{message}</div>}
        </FormattedMessage>

        <div style={{ maxWidth: 140 }}>
          <DatePicker
            value={values && values.to}
            onChange={(date: Date) => {
              const newStatements = statements.map((statement, index) => {
                return {
                  ...statement,
                  object: {
                    ...statement.object,
                    ...(index === statementIndex ? { to: date } : null),
                  },
                }
              })
              this.props.onChangeStatements(newStatements)
            }}
            locale={locale}
          />
        </div>
      </div>
    )
  }
}

export default withRuntimeContext(injectIntl(SelectConditions))

```


[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/admin/pages/Form/utils.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { DataProxy } from 'apollo-cache'
import { RouteFormData } from 'pages'
import { pathOr } from 'ramda'

import Routes from '../../../../queries/Routes.graphql'
import Route from '../../../../queries/Route.graphql'
import { DateVerbOptions } from '../../../../utils/conditions/typings'
import {
  DateInfoFormat,
  DateStatementFormat,
  DeleteMutationResult,
  QueryData,
  RoutesQuery,
  SaveMutationResult,
} from './typings'
import { DATA_SOURCE } from '../consts'

const routesCacheAccessParameters = (bindingId?: string) => ({
  query: Routes,
  variables: {
    domain: 'store',
    bindingId,
  },
})

const routeCacheAccessParameters = (routeId: string, bindingId?: string) => ({
  query: Route,
  variables: {
    domain: 'store',
    routeId,
    bindingId,
  },
})

const logCacheError = () => {
  console.warn('No cache found for "Routes".')
}

const readRoutesFromStore = (store: DataProxy, binding?: string): QueryData =>
  store.readQuery(routesCacheAccessParameters(binding))

const writeRoutesToStore = (
  newData: RoutesQuery,
  store: DataProxy,
  bindingId?: string,
  updatedRoute?: Route
) => {
  store.writeQuery({
    data: newData,
    ...routesCacheAccessParameters(bindingId),
  })
  if (updatedRoute) {
    store.writeQuery({
      data: { route: updatedRoute },
      ...routeCacheAccessParameters(updatedRoute.routeId, bindingId),
    })
  }
}

export const updateStoreAfterDelete = (binding?: string) => (
  store: DataProxy,
  result: DeleteMutationResult
) => {
  const deletedPageId = result.data && result.data.deleteRoute

  try {
    const queryData = readRoutesFromStore(store, binding)

    if (queryData) {
      const routes = queryData.routes

      const newRoutes = routes.filter(({ uuid }) => uuid !== deletedPageId)

      const newData = {
        ...queryData,
        routes: newRoutes || routes,
      }

      writeRoutesToStore(newData, store, binding)
    }
  } catch (err) {
    logCacheError()
  }
}

export const updateStoreAfterSave = (
  store: DataProxy,
  result: SaveMutationResult
) => {
  const savedRoute = result.data && result.data.saveRoute

  try {
    const binding = savedRoute?.binding
    const queryData = readRoutesFromStore(store, binding)

    if (queryData && savedRoute) {
      const routes = queryData.routes

      const savedRouteId = savedRoute && savedRoute.uuid
      const matchedRoute = routes.find(route => route.uuid === savedRouteId)
      if (matchedRoute) {
        const idx = routes.indexOf(matchedRoute)
        routes[idx] = savedRoute
      } else {
        routes.push(savedRoute)
      }

      const newData = {
        ...queryData,
        routes,
      }

      writeRoutesToStore(newData, store, binding, savedRoute)
    }
  } catch (err) {
    logCacheError()
  }
}

const getConditionStatementObject = (
  objectJson: string,
  verb: DateVerbOptions
): Partial<DateStatementFormat> => {
  const dateInfoStringValues: DateInfoFormat = JSON.parse(objectJson)

  return {
    between: {
      from: new Date(dateInfoStringValues.from),
      to: new Date(dateInfoStringValues.to),
    },
    from: {
      date: new Date(dateInfoStringValues.from),
    },
    is: {
      date: new Date(dateInfoStringValues.from),
    },
    to: {
      date: new Date(dateInfoStringValues.to),
    },
  }[verb]
}

export const formatToFormData = (route: Route): RouteFormData => {
  return {
    ...route,
    metaTagDescription: pathOr('', ['metaTags', 'description'], route),
    metaTagKeywords: pathOr<string[], string[]>(
      [],
      ['metaTags', 'keywords'],
      route
    ).map(keyword => ({ label: keyword, value: keyword })),
    metaTagRobots: pathOr('', ['metaTags', 'robots'], route),
    pages: route.pages.map((page, index) => ({
      ...page,
      condition: {
        ...page.condition,
        statements: page.condition.statements.map(
          ({ verb, subject, objectJSON }) => ({
            error: '',
            object: getConditionStatementObject(
              objectJSON,
              verb as DateVerbOptions
            ),
            subject,
            verb,
          })
        ),
      },
      operator: page.condition.allMatches ? 'all' : 'any',
      uniqueId: index,
    })),
    dataSource: DATA_SOURCE,
  }
}

export const generateNewRouteId = (interfaceId: string, path: string) => {
  return `${interfaceId}#${path.replace(
    /\//gi,
    (_, offset: number, fullString: string) => {
      return offset === 0 || offset === fullString.length - 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/admin/pages/Form/typings.d.ts from the admin-pages VTEX app.

[Assistant]
Here's the implementation:

```typescript
import { ConditionsProps } from 'vtex.styleguide'

export interface DeleteMutationResult {
  data?: {
    deleteRoute: string
  }
}

export interface SaveMutationResult {
  data?: {
    saveRoute: Route
  }
}

export interface TemplateMutationResult {
  data?: {
    availableTemplates: Template[]
  }
}

export type QueryData = RoutesQuery | null

export interface RoutesQuery {
  routes: Route[]
}

export interface ClientSideUniqueId {
  uniqueId: number
  operator: ConditionsProps['operator']
}

interface StatementsOnSaveRoute {
  subject: string
  verb: string
  objectJSON: string
}

interface ConditionsOnSaveRouteVariables {
  id?: string
  allMatches: boolean
  statements: StatementsOnSaveRoute[]
}

export interface SaveRouteVariables {
  route: Partial<Route> &
    Pick<Route, 'declarer' | 'domain' | 'path' | 'routeId' | 'binding'>
}

export interface DeleteRouteVariables {
  uuid: string
}

export interface DateInfoFormat {
  date: string
  to: string
  from: string
}

export type DateStatementFormat = Record<keyof DateInfoFormat, Date>

export type FormErrors = Omit<Partial<Route>, 'pages'> & {
  pages?: {
    [key: string]: {
      template?: string
      condition?: string
    }
  }
}

```