import { Box, Divider, Dropdown, Text, TextButton, TableListItem, ListItemAction, listItemSelectBuilder, SectionHelper, } from '@wix/design-system'; import { Add, ArrowRight } from '@wix/wix-ui-icons-common'; import { ImportState, NewFieldType } from '@wix/bex-core'; import { useTranslations } from '@wix/patterns-fields'; import React from 'react'; import { observer } from 'mobx-react-lite'; import { IdCollisionControl } from './ImportIdCollisionControl'; export interface ImportStepConfigurationProps { state: ImportState; } const COLUMN_WIDTHS = { title: 'minmax(0, 150px)', arrow: '1fr', field: 'minmax(0, 250px)', }; const SCROLLBAR_GUTTER: React.CSSProperties = { scrollbarGutter: 'stable' }; // Synthetic dropdown option id representing "create a new field from this // column". Distinct from any real field id (which never starts with `$`). const CREATE_NEW_FIELD_OPTION_ID = '$createNewField'; // Translation key for each new-field type's name, injected (lowercased) into the // helper text ("This will be a new {typeName} field."). Reuses the shared // patterns-fields field-type names. Keyed by `NewFieldType` so adding a detected // type makes the missing entry a compile error here — the single place to extend. type FieldTypeTitleKey = | 'auto-patterns.fields.field_type_text_title' | 'auto-patterns.fields.field_type_number_title' | 'auto-patterns.fields.field_type_date_title' | 'auto-patterns.fields.field_type_boolean_title' | 'auto-patterns.fields.field_type_url_title' | 'auto-patterns.fields.field_type_image_title' | 'auto-patterns.fields.field_type_video_title' | 'auto-patterns.fields.field_type_document_title' | 'auto-patterns.fields.field_type_audio_title'; const NEW_FIELD_TYPE_TITLE_KEY: Record = { text: 'auto-patterns.fields.field_type_text_title', number: 'auto-patterns.fields.field_type_number_title', date: 'auto-patterns.fields.field_type_date_title', boolean: 'auto-patterns.fields.field_type_boolean_title', url: 'auto-patterns.fields.field_type_url_title', image: 'auto-patterns.fields.field_type_image_title', video: 'auto-patterns.fields.field_type_video_title', document: 'auto-patterns.fields.field_type_document_title', audio: 'auto-patterns.fields.field_type_audio_title', }; type DropdownOption = ReturnType; interface NewFieldHelperTextProps { state: ImportState; header: string; fieldType: NewFieldType; } /** * Helper line under a "create new field" mapping (e.g. "This will be a new text * field.") followed by an "Edit field" link that opens the field editor. The * type name comes from the shared patterns-fields translations via * `useTranslations`, so this is only rendered when the import modal is wrapped in * `PatternsFieldsProvider` — which `ImportButtonModalLayout` does whenever field * creation is enabled (the only case a `new` mapping exists). */ const NewFieldHelperText = observer(function _NewFieldHelperText({ state, header, fieldType, }: NewFieldHelperTextProps) { const { t: ft } = useTranslations(); const typeName = ft(NEW_FIELD_TYPE_TITLE_KEY[fieldType]).toLocaleLowerCase(); return ( {state.translate('cairo.import.newFieldHelper', { typeName })} state.beginFieldEdit(header)} > {state.translate('cairo.import.editField')} ); }); interface FieldMappingCellProps { state: ImportState; header: string; fieldOptions: DropdownOption[]; } const FieldMappingCell = observer(function _FieldMappingCell({ state, header, fieldOptions, }: FieldMappingCellProps) { const { translate: t, selectedFieldIds, canCreateNewField } = state; const mapping = state.mappingByHeader[header]; const selectedId = mapping?.kind === 'existing' ? mapping.fieldId : mapping?.kind === 'new' ? CREATE_NEW_FIELD_OPTION_ID : undefined; const options: DropdownOption[] = [ ...fieldOptions.map((option) => ({ ...option, disabled: selectedFieldIds.has(String(option.id)) && !( mapping?.kind === 'existing' && mapping.fieldId === String(option.id) ), })), ...(canCreateNewField ? [ // The new field's name — the display name the user entered in the // field editor once they've named it, otherwise the CSV column header // (the default the field is created with). Followed by a translated // "(New)" marker, built in code rather than via an interpolated key so // it always shows regardless of which translation bundle the consumer // loads. (() => { const newFieldName = mapping?.kind === 'new' && mapping.displayName ? mapping.displayName : header; const label = `${newFieldName} ${t('cairo.import.newFieldSuffix')}`; return listItemSelectBuilder({ id: CREATE_NEW_FIELD_OPTION_ID, label, title: label, }); })(), ] : []), ]; return ( { state.setMapping(header, null); }} placeholder={t('cairo.import.skipColumn')} selectedId={selectedId} onSelect={({ id }) => { if (id === CREATE_NEW_FIELD_OPTION_ID) { state.setNewFieldMapping(header); } else { state.setMapping(header, id as string); } }} popoverProps={{ appendTo: 'window', zIndex: 5001, flip: true, fixed: false, }} options={options} fixedFooter={ canCreateNewField ? (
{/* Separate the "Create new field" action from the field options above it (CAIRO-4476). */} } dataHook={`import-create-field-${header}`} title={t('cairo.import.createNewField')} // Map the column to a new field, then open the editor on it — the // editor component (renderFieldEditor) reacts to `editingHeader`. onClick={() => { state.setNewFieldMapping(header); state.beginFieldEdit(header); }} />
) : undefined } size="small" /> {mapping?.kind === 'new' && ( )} {state.canConfigureWritePolicy && state.idMappedHeader === header && ( )}
); }); function _ImportStepConfiguration({ state }: ImportStepConfigurationProps) { const { csvHeaders, fields, translate: t } = state; // Row order is snapshotted per file in state, with the `_id`-mapped column // floated to the top (CAIRO-4418). It stays fixed while the user edits // mappings, so clearing/remapping the `_id` column doesn't reorder the row // under the cursor. Fall back to CSV order if the snapshot isn't populated. const orderedHeaders = state.orderedHeaders.length > 0 ? state.orderedHeaders : csvHeaders; const baseDropdownOptions = fields .filter((field) => field.id != null) .map((field) => listItemSelectBuilder({ id: field.id!, label: field.header, title: field.header, }), ); const hasNoRows = state.hasNoMappedData; return ( {hasNoRows && ( {t('cairo.import.noRowsWarningDescription')} )} {t('cairo.import.columnTitle')} {t('cairo.import.collectionField')} {orderedHeaders.map((header, index) => ( {header} ), width: COLUMN_WIDTHS.title, }, { value: ( ), width: COLUMN_WIDTHS.arrow, }, { value: ( ), width: COLUMN_WIDTHS.field, }, ]} /> ))} ); } export const ImportStepConfiguration = observer(_ImportStepConfiguration);