import React, { useCallback, useEffect } from "react"; import { CollectionActionsProps, EntityCollectionTable, getFieldConfig, getPropertiesWithPropertiesOrder, getPropertyInPath, PropertiesOrBuilders, Property, PropertyConfigBadge, resolveCollection, ResolvedProperties, slugify, useAuthController, useCustomizationController, useNavigationController, User, useSelectionController, useSnackbarController, useTranslation } from "@firecms/core"; import { Button, cls, defaultBorderMixin, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Select, SelectItem, Tooltip, Typography, UploadIcon, } from "@firecms/ui"; import { buildEntityPropertiesFromData } from "@firecms/schema_inference"; import { useImportConfig } from "../hooks"; import { convertDataToEntity, getInferenceType } from "../utils"; import { DataNewPropertiesMapping, ImportFileUpload, ImportSaveInProgress } from "../components"; import { ImportConfig } from "../types"; type ImportState = "initial" | "mapping" | "preview" | "import_data_saving"; export function ImportCollectionAction, USER extends User>({ collection, path, pathSegments, onAnalyticsEvent }: CollectionActionsProps & { onAnalyticsEvent?: (event: string, params?: any) => void; } ) { const authController = useAuthController(); const customizationController = useCustomizationController(); const snackbarController = useSnackbarController(); const [open, setOpen] = React.useState(false); const [step, setStep] = React.useState("initial"); const importConfig = useImportConfig(); const { t } = useTranslation(); const handleClickOpen = useCallback(() => { setOpen(true); onAnalyticsEvent?.("import_open"); setStep("initial"); }, [onAnalyticsEvent]); const handleClose = useCallback(() => { setOpen(false); }, [setOpen]); const onMappingComplete = useCallback(() => { onAnalyticsEvent?.("import_mapping_complete"); setStep("preview"); }, [onAnalyticsEvent]); const onPreviewComplete = useCallback(() => { onAnalyticsEvent?.("import_data_save"); setStep("import_data_saving"); }, [onAnalyticsEvent]); const onDataAdded = async (data: object[]) => { importConfig.setImportData(data); if (data.length > 0) { const originProperties = await buildEntityPropertiesFromData(data, getInferenceType); importConfig.setOriginProperties(originProperties); const headersMapping = buildHeadersMappingFromData(data, collection?.properties); importConfig.setHeadersMapping(headersMapping); const firstKey = Object.keys(headersMapping)?.[0]; if (firstKey?.includes("id") || firstKey?.includes("key")) { importConfig.setIdColumn(firstKey); } } setTimeout(() => { onAnalyticsEvent?.("import_data_added"); setStep("mapping"); }, 100); }; const resolvedCollection = resolveCollection({ collection, path, propertyConfigs: customizationController.propertyConfigs, authController }); const properties = getPropertiesWithPropertiesOrder(resolvedCollection.properties, resolvedCollection.propertiesOrder as Extract[]) as ResolvedProperties; const propertiesAndLevel = Object.entries(properties) .flatMap(([key, property]) => getPropertiesAndLevel(key, property, 0)); const propertiesOrder = (resolvedCollection.propertiesOrder ?? Object.keys(resolvedCollection.properties)) as Extract[]; if (collection.collectionGroup) { return null; } return <> {/* A column, so the content fills the space between the title and the actions and grows past it when the mapping list is long, leaving the sticky actions at the bottom */} {/* No gutter: flex margins don't collapse, and the content's margin already spaces it */} {step === "initial" && <> {t("upload_file_description")} } {step === "mapping" && <> { return { importConfig.setIdColumn(importKey); }} onPropertySelected={(newPropertyKey) => { onAnalyticsEvent?.("import_mapping_field_updated"); const newHeadersMapping: Record = Object.entries(importConfig.headersMapping) .map(([currentImportKey, currentPropertyKey]) => { if (currentPropertyKey === newPropertyKey) { return { [currentImportKey]: null }; } if (currentImportKey === importKey) { return { [currentImportKey]: newPropertyKey }; } return { [currentImportKey]: currentPropertyKey }; }) .reduce((acc, curr) => ({ ...acc, ...curr }), {}); importConfig.setHeadersMapping(newHeadersMapping as Record); if (newPropertyKey === importConfig.idColumn) { importConfig.setIdColumn(undefined); } }} />; }}/> } {step === "preview" && } {step === "import_data_saving" && importConfig && { handleClose(); snackbarController.open({ type: "info", message: t("data_imported_successfully") }); }} />} {step === "mapping" && } {step === "preview" && } {step === "mapping" && } {step === "preview" && } ; } const internalIDValue = "__internal_id__"; function PropertyTreeSelect({ selectedPropertyKey, properties, onPropertySelected, onIdSelected, propertiesAndLevel, isIdColumn }: { selectedPropertyKey: string | null; properties: Record; onPropertySelected: (propertyKey: string | null) => void; onIdSelected: () => void; propertiesAndLevel: PropertyAndLevel[]; isIdColumn?: boolean; }) { const { t } = useTranslation(); const selectedProperty = selectedPropertyKey ? getPropertyInPath(properties, selectedPropertyKey) : null; const renderValue = useCallback((selectedPropertyKey: string) => { if (selectedPropertyKey === internalIDValue) { return {t("use_column_as_id")}; } if (!selectedPropertyKey || !selectedProperty) { return {t("do_not_import_property")} ; } return ; }, [selectedProperty]); const onSelectValueChange = (value: string) => { if (value === internalIDValue) { onIdSelected(); onPropertySelected(null); } else if (value === "__do_not_import") { onPropertySelected(null); } else { onPropertySelected(value); } }; return ; } type PropertyAndLevel = { property: Property, level: number, propertyKey: string }; function getPropertiesAndLevel(key: string, property: Property, level: number): PropertyAndLevel[] { const properties: PropertyAndLevel[] = []; properties.push({ property, level, propertyKey: key }); if (property.dataType === "map" && property.properties) { Object.entries(property.properties).forEach(([childKey, value]) => { properties.push(...getPropertiesAndLevel(`${key}.${childKey}`, value as Property, level + 1)); }); } return properties; } export function PropertySelectEntry({ propertyKey, property, level = 0 }: { propertyKey: string; property: Property; level?: number; }) { const { propertyConfigs } = useCustomizationController(); const widget = getFieldConfig(property, propertyConfigs); return
{new Array(level).fill(0).map((_, index) =>
)}
{property.name ? property.name : "\u00a0" } {propertyKey}
; } export function ImportDataPreview>({ importConfig, properties, propertiesOrder }: { importConfig: ImportConfig, properties: ResolvedProperties, propertiesOrder: Extract[], }) { const { t } = useTranslation(); const authController = useAuthController(); const navigation = useNavigationController(); useEffect(() => { const mappedData = importConfig.importData.map(d => convertDataToEntity( authController, navigation, d, importConfig.idColumn, importConfig.headersMapping, properties, "TEMP_PATH", importConfig.defaultValues )); importConfig.setEntities(mappedData); }, []); const selectionController = useSelectionController(); return {t("imported_data_preview")} {t("entities_will_be_overwritten")}
} tableController={{ data: importConfig.entities, dataLoading: false, noMoreToLoad: false }} enablePopupIcon={false} endAdornment={
} filterable={false} sortable={false} openEntityMode={"full_screen"} selectionController={selectionController} properties={properties}/> } function buildHeadersMappingFromData(objArr: object[], properties?: PropertiesOrBuilders) { const headersMapping: Record = {}; objArr.filter(Boolean).forEach((obj) => { Object.keys(obj).forEach((key) => { // @ts-ignore const child = obj[key]; if (typeof child === "object" && !Array.isArray(child)) { const childProperty = properties?.[key]; const childProperties = childProperty && "properties" in childProperty ? childProperty.properties : undefined; const childHeadersMapping = buildHeadersMappingFromData([child], childProperties); Object.entries(childHeadersMapping).forEach(([subKey, mapping]) => { headersMapping[`${key}.${subKey}`] = `${key}.${mapping}`; }); } if (!properties) { headersMapping[key] = key; } else if (key in properties) { headersMapping[key] = key; } else { const slug = slugify(key); if (slug in properties) { headersMapping[key] = slug; } else { headersMapping[key] = key; } } }); }); return headersMapping; }