diff --git a/src/features/import/components/Configure/Configuration/IdConfig.tsx b/src/features/import/components/Configure/Configuration/IdConfig.tsx new file mode 100644 index 0000000000..f36a4e60ce --- /dev/null +++ b/src/features/import/components/Configure/Configuration/IdConfig.tsx @@ -0,0 +1,135 @@ +import { FC } from 'react'; +import { + Alert, + Box, + Divider, + Radio, + RadioGroup, + Typography, + useTheme, +} from '@mui/material'; + +import { IDFieldColumn } from 'features/import/utils/types'; +import messageIds from 'features/import/l10n/messageIds'; +import { Msg } from 'core/i18n'; +import { UIDataColumn } from 'features/import/hooks/useUIDataColumns'; + +interface IdConfigProps { + uiDataColumn: UIDataColumn; +} + +const IdConfig: FC = ({ uiDataColumn }) => { + const theme = useTheme(); + + return ( + + + + + + + + + + + { + if ( + event.target.value == 'ext_id' || + event.target.value == 'id' + ) { + uiDataColumn.updateIdField(event.target.value); + } + }} + sx={{ paddingRight: 1 }} + value="ext_id" + /> + + + + + + + + + + + + + + + + + { + if ( + event.target.value == 'ext_id' || + (event.target.value == 'id' && !uiDataColumn.wrongIDFormat) + ) { + uiDataColumn.updateIdField(event.target.value); + } + }} + sx={{ paddingRight: 1 }} + value="id" + /> + + + + + + + + + + + + + + + + {uiDataColumn.wrongIDFormat && ( + + + + )} + + ); +}; + +export default IdConfig; diff --git a/src/features/import/components/Configure/Configuration/OrgConfig.tsx b/src/features/import/components/Configure/Configuration/OrgConfig.tsx new file mode 100644 index 0000000000..ddf9536b6b --- /dev/null +++ b/src/features/import/components/Configure/Configuration/OrgConfig.tsx @@ -0,0 +1,79 @@ +import { FC } from 'react'; +import { Box, Divider, Typography } from '@mui/material'; + +import messageIds from 'features/import/l10n/messageIds'; +import { OrgColumn } from 'features/import/utils/types'; +import OrgConfigRow from './OrgConfigRow'; +import { UIDataColumn } from 'features/import/hooks/useUIDataColumns'; +import useOrganizations from 'features/organizations/hooks/useOrganizations'; +import { Msg, useMessages } from 'core/i18n'; + +interface OrgConfigProps { + uiDataColumn: UIDataColumn; +} + +const OrgConfig: FC = ({ uiDataColumn }) => { + const messages = useMessages(messageIds); + const organizations = useOrganizations(); + + if (!organizations.data) { + return null; + } + + return ( + + + + + + + + {messages.configuration.configure.orgs.status().toLocaleUpperCase()} + + + + + {messages.configuration.configure.orgs + .organizations() + .toLocaleUpperCase()} + + + + {uiDataColumn.uniqueValues.map((uniqueValue, index) => ( + <> + {index != 0 && } + uiDataColumn.deselectOrg(uniqueValue)} + onSelectOrg={(orgId) => uiDataColumn.selectOrg(orgId, uniqueValue)} + orgs={organizations.data || []} + selectedOrgId={uiDataColumn.getSelectedOrgId(uniqueValue)} + title={uniqueValue.toString()} + /> + + ))} + {uiDataColumn.numberOfEmptyRows > 0 && ( + <> + + uiDataColumn.deselectOrg(null)} + onSelectOrg={(orgId) => uiDataColumn.selectOrg(orgId, null)} + orgs={organizations.data || []} + selectedOrgId={uiDataColumn.getSelectedOrgId(null)} + title={messages.configuration.configure.tags.empty()} + /> + + )} + + ); +}; + +export default OrgConfig; diff --git a/src/features/import/components/Configure/Configuration/OrgConfigRow.tsx b/src/features/import/components/Configure/Configuration/OrgConfigRow.tsx new file mode 100644 index 0000000000..c2e6aa1159 --- /dev/null +++ b/src/features/import/components/Configure/Configuration/OrgConfigRow.tsx @@ -0,0 +1,106 @@ +import { FC } from 'react'; +import { ArrowForward, Delete } from '@mui/icons-material'; +import { + Box, + FormControl, + IconButton, + InputLabel, + MenuItem, + Select, + Typography, + useTheme, +} from '@mui/material'; + +import messageIds from 'features/import/l10n/messageIds'; +import { ZetkinOrganization } from 'utils/types/zetkin'; +import { Msg, useMessages } from 'core/i18n'; + +interface OrgConfigRowProps { + italic?: boolean; + numRows: number; + onSelectOrg: (orgId: number) => void; + onDeselectOrg: () => void; + orgs: Pick[]; + selectedOrgId: number | null; + title: string; +} + +const OrgConfigRow: FC = ({ + italic, + numRows, + onSelectOrg, + onDeselectOrg, + orgs, + selectedOrgId, + title, +}) => { + const theme = useTheme(); + const messages = useMessages(messageIds); + return ( + + + + + {title} + + + + + + + + + + + { + if (selectedOrgId) { + onDeselectOrg(); + } + }} + > + + + + + + + + + ); +}; + +export default OrgConfigRow; diff --git a/src/features/import/components/Configure/Configuration/TagConfig.tsx b/src/features/import/components/Configure/Configuration/TagConfig.tsx new file mode 100644 index 0000000000..aeefb8df2b --- /dev/null +++ b/src/features/import/components/Configure/Configuration/TagConfig.tsx @@ -0,0 +1,79 @@ +import { FC } from 'react'; +import { Box, Divider, Typography } from '@mui/material'; + +import messageIds from 'features/import/l10n/messageIds'; +import { TagColumn } from 'features/import/utils/types'; +import TagConfigRow from './TagConfigRow'; +import { UIDataColumn } from 'features/import/hooks/useUIDataColumns'; +import { ZetkinTag } from 'utils/types/zetkin'; +import { Msg, useMessages } from 'core/i18n'; + +interface TagConfigProps { + uiDataColumn: UIDataColumn; +} + +const TagConfig: FC = ({ uiDataColumn }) => { + const messages = useMessages(messageIds); + return ( + + + + + + + + {uiDataColumn.title.toLocaleUpperCase()} + + + + + {messages.configuration.configure.tags + .tagsHeader() + .toLocaleUpperCase()} + + + + {uiDataColumn.uniqueValues.map((uniqueValue, index) => ( + <> + {index != 0 && } + + uiDataColumn.assignTag(tag.id, uniqueValue) + } + onUnassignTag={(tag: ZetkinTag) => + uiDataColumn.unAssignTag(tag.id, uniqueValue) + } + title={uniqueValue.toString()} + /> + + ))} + {uiDataColumn.numberOfEmptyRows > 0 && ( + <> + + + uiDataColumn.assignTag(tag.id, null) + } + onUnassignTag={(tag: ZetkinTag) => + uiDataColumn.unAssignTag(tag.id, null) + } + title={messages.configuration.configure.tags.empty()} + /> + + )} + + ); +}; + +export default TagConfig; diff --git a/src/features/import/components/Configure/Configuration/TagConfigRow.tsx b/src/features/import/components/Configure/Configuration/TagConfigRow.tsx new file mode 100644 index 0000000000..90e42e627f --- /dev/null +++ b/src/features/import/components/Configure/Configuration/TagConfigRow.tsx @@ -0,0 +1,60 @@ +import { ArrowForward } from '@mui/icons-material'; +import { FC } from 'react'; +import { Box, Typography } from '@mui/material'; + +import messageIds from 'features/import/l10n/messageIds'; +import { Msg } from 'core/i18n'; +import TagManager from 'features/tags/components/TagManager'; +import { ZetkinTag } from 'utils/types/zetkin'; + +interface TagConfigRowProps { + assignedTags: ZetkinTag[]; + italic?: boolean; + numRows: number; + onAssignTag: (tag: ZetkinTag) => void; + onUnassignTag: (tag: ZetkinTag) => void; + title: string; +} + +const TagConfigRow: FC = ({ + assignedTags, + italic, + numRows, + onAssignTag, + onUnassignTag, + title, +}) => { + return ( + + + + + {title} + + + + + onAssignTag(tag)} + onUnassignTag={(tag) => onUnassignTag(tag)} + /> + + + + + + + ); +}; + +export default TagConfigRow; diff --git a/src/features/import/components/Configure/Configuration/index.tsx b/src/features/import/components/Configure/Configuration/index.tsx new file mode 100644 index 0000000000..8d945bea29 --- /dev/null +++ b/src/features/import/components/Configure/Configuration/index.tsx @@ -0,0 +1,65 @@ +import { CompareArrows } from '@mui/icons-material'; +import { FC } from 'react'; +import { Box, useTheme } from '@mui/material'; + +import IdConfig from './IdConfig'; +import messageIds from 'features/import/l10n/messageIds'; +import OrgConfig from './OrgConfig'; +import TagConfig from './TagConfig'; +import { UIDataColumn } from 'features/import/hooks/useUIDataColumns'; +import { useMessages } from 'core/i18n'; +import ZUIEmptyState from 'zui/ZUIEmptyState'; +import { + Column, + ColumnKind, + IDFieldColumn, + OrgColumn, + TagColumn, +} from 'features/import/utils/types'; + +interface ConfigurationProps { + uiDataColumn: UIDataColumn | null; +} + +const Configuration: FC = ({ uiDataColumn }) => { + const messages = useMessages(messageIds); + const theme = useTheme(); + + return ( + + {uiDataColumn && uiDataColumn.originalColumn.kind == ColumnKind.TAG && ( + } /> + )} + {uiDataColumn && + uiDataColumn.originalColumn.kind == ColumnKind.ID_FIELD && ( + } + /> + )} + {uiDataColumn && + uiDataColumn.originalColumn.kind == ColumnKind.ORGANIZATION && ( + } /> + )} + {!uiDataColumn && ( + + } + /> + + )} + + ); +}; + +export default Configuration; diff --git a/src/features/import/components/Configure/Mapping/MappingRow.tsx b/src/features/import/components/Configure/Mapping/MappingRow.tsx new file mode 100644 index 0000000000..13ce5484b3 --- /dev/null +++ b/src/features/import/components/Configure/Mapping/MappingRow.tsx @@ -0,0 +1,212 @@ +import { FC } from 'react'; +import { ArrowForward, ChevronRight } from '@mui/icons-material'; +import { + Box, + Button, + Checkbox, + FormControl, + InputLabel, + MenuItem, + Select, + Typography, + useTheme, +} from '@mui/material'; + +import messageIds from 'features/import/l10n/messageIds'; +import { UIDataColumn } from 'features/import/hooks/useUIDataColumns'; +import { Column, ColumnKind } from 'features/import/utils/types'; +import { Msg, useMessages } from 'core/i18n'; + +interface MappingRowProps { + clearConfiguration: () => void; + column: UIDataColumn; + columnOptions: { label: string; value: string }[]; + isBeingConfigured: boolean; + onChange: (newColumn: Column) => void; + onConfigureStart: () => void; + optionAlreadySelected: (value: string) => boolean; +} + +const MappingRow: FC = ({ + clearConfiguration, + column, + columnOptions, + isBeingConfigured, + onChange, + onConfigureStart, + optionAlreadySelected, +}) => { + const theme = useTheme(); + const messages = useMessages(messageIds); + + const getValue = () => { + if (column.originalColumn.kind == ColumnKind.FIELD) { + return `field:${column.originalColumn.field}`; + } + + if (column.originalColumn.kind != ColumnKind.UNKNOWN) { + return column.originalColumn.kind.toString(); + } + + //Column kind is UNKNOWN, so we want no selected value + return ''; + }; + + return ( + + + + { + if (isChecked) { + onChange({ + ...column.originalColumn, + selected: isChecked, + }); + } else { + onChange({ + ...column.originalColumn, + kind: ColumnKind.UNKNOWN, + selected: isChecked, + }); + } + clearConfiguration(); + }} + /> + + {column.title} + + + + + + + + + + + + + + {column.showColumnValuesMessage && ( + + + {column.columnValuesMessage} + + + )} + + {column.showNeedsConfigMessage && ( + + )} + {column.showMappingResultMessage && + column.renderMappingResultsMessage()} + + {(column.showNeedsConfigMessage || column.showMappingResultMessage) && ( + + )} + + + ); +}; + +export default MappingRow; diff --git a/src/features/import/components/Configure/Mapping/index.tsx b/src/features/import/components/Configure/Mapping/index.tsx new file mode 100644 index 0000000000..12d404fc47 --- /dev/null +++ b/src/features/import/components/Configure/Mapping/index.tsx @@ -0,0 +1,79 @@ +import { FC } from 'react'; +import { Box, Divider, Typography } from '@mui/material'; + +import { Column } from 'features/import/utils/types'; +import MappingRow from './MappingRow'; +import messageIds from 'features/import/l10n/messageIds'; +import { UIDataColumn } from 'features/import/hooks/useUIDataColumns'; +import useColumnMutations from 'features/import/hooks/useColumnMutations'; +import useColumnOptions from 'features/import/hooks/useColumnOptions'; +import { useNumericRouteParams } from 'core/hooks'; +import useSelectedOptions from 'features/import/hooks/useSelectedOptions'; +import { Msg, useMessages } from 'core/i18n'; + +interface MappingProps { + clearConfiguration: () => void; + columns: UIDataColumn[]; + columnIndexBeingConfigured: number | null; + onConfigureStart: (columnIndex: number) => void; +} + +const Mapping: FC = ({ + clearConfiguration, + columns, + columnIndexBeingConfigured, + onConfigureStart, +}) => { + const { orgId } = useNumericRouteParams(); + const messages = useMessages(messageIds); + const columnOptions = useColumnOptions(orgId); + const { updateColumn } = useColumnMutations(); + const optionAlreadySelected = useSelectedOptions(); + + return ( + + + + + + + + {messages.configuration.mapping.fileHeader().toUpperCase()} + + + + + {messages.configuration.mapping.zetkinHeader().toUpperCase()} + + + + + {columns.map((column, index) => { + return ( + + {index == 0 && } + updateColumn(index, column)} + onConfigureStart={() => onConfigureStart(index)} + optionAlreadySelected={optionAlreadySelected} + /> + + + ); + })} + + + ); +}; + +export default Mapping; diff --git a/src/features/import/components/Configure/SheetSettings/index.tsx b/src/features/import/components/Configure/SheetSettings/index.tsx new file mode 100644 index 0000000000..fea70edf8d --- /dev/null +++ b/src/features/import/components/Configure/SheetSettings/index.tsx @@ -0,0 +1,107 @@ +import { ExpandMore } from '@mui/icons-material'; +import { + AccordionDetails, + AccordionSummary, + Box, + Checkbox, + FormControl, + FormHelperText, + InputLabel, + MenuItem, + Select, + styled, + Typography, + useTheme, +} from '@mui/material'; +import { FC, useState } from 'react'; +import MuiAccordion, { AccordionProps } from '@mui/material/Accordion'; + +import messageIds from 'features/import/l10n/messageIds'; +import useSheets from 'features/import/hooks/useSheets'; +import { Msg, useMessages } from 'core/i18n'; + +const Accordion = styled((props: AccordionProps) => ( + +))(() => ({ + '&:before': { + display: 'none', + }, + border: 0, +})); + +interface SheetSettingsProps { + clearConfiguration: () => void; +} + +const SheetSettings: FC = ({ clearConfiguration }) => { + const messages = useMessages(messageIds); + const theme = useTheme(); + const { + firstRowIsHeaders, + selectedSheetIndex, + sheets, + updateFirstRowIsHeaders, + updateSelectedSheetIndex, + } = useSheets(); + const [settingsExpanded, setSettingsExpanded] = useState(true); + + return ( + setSettingsExpanded(isExpanded)} + > + } + > + + + + + + {settingsExpanded + ? messages.configuration.hide().toLocaleUpperCase() + : messages.configuration.show().toLocaleUpperCase()} + + + + + {sheets.length > 1 && ( + + + + + + + + + + )} + + updateFirstRowIsHeaders(isChecked)} + /> + + + + + + + ); +}; + +export default SheetSettings; diff --git a/src/features/import/components/Configure/index.tsx b/src/features/import/components/Configure/index.tsx new file mode 100644 index 0000000000..c262480239 --- /dev/null +++ b/src/features/import/components/Configure/index.tsx @@ -0,0 +1,53 @@ +import { Box } from '@mui/material'; +import { FC, useState } from 'react'; + +import Configuration from './Configuration'; +import Mapping from './Mapping'; +import SheetSettings from './SheetSettings'; +import { useNumericRouteParams } from 'core/hooks'; +import useUIDataColumns from 'features/import/hooks/useUIDataColumns'; + +const Configure: FC = () => { + const [columnIndexBeingConfigured, setColumnIndexBeingConfigured] = useState< + number | null + >(null); + const { orgId } = useNumericRouteParams(); + const uiDataColumns = useUIDataColumns(orgId); + + return ( + + + + setColumnIndexBeingConfigured(null)} + /> + setColumnIndexBeingConfigured(null)} + columnIndexBeingConfigured={columnIndexBeingConfigured} + columns={uiDataColumns} + onConfigureStart={(columnIndex: number) => + setColumnIndexBeingConfigured(columnIndex) + } + /> + + + + + + Preview + + ); +}; + +export default Configure; diff --git a/src/features/import/components/Importer.tsx b/src/features/import/components/Importer.tsx index d0d0bef144..34503860d3 100644 --- a/src/features/import/components/Importer.tsx +++ b/src/features/import/components/Importer.tsx @@ -13,19 +13,19 @@ import { } from '@mui/material'; import { FC, useState } from 'react'; +import Configure from './Configure'; import messageIds from 'features/import/l10n/messageIds'; import { Msg } from 'core/i18n'; import Validation from './Validation'; interface ImporterProps { onClose: () => void; - onRestart: () => void; open: boolean; } type ImportSteps = 0 | 1 | 2 | 3; -const Importer: FC = ({ onRestart, open, onClose }) => { +const Importer: FC = ({ open, onClose }) => { const theme = useTheme(); const fullScreen = useMediaQuery(theme.breakpoints.down('md')); const [activeStep, setActiveStep] = useState(1); @@ -88,60 +88,49 @@ const Importer: FC = ({ onRestart, open, onClose }) => { - - {activeStep === 2 && ( - setActiveStep(0)} - onDisabled={(value) => setDisabled(value)} - /> - )} - - - - This message will depend on the state of the import. - - - + + This message will depend on the state of the import. + + + + diff --git a/src/features/import/hooks/useColumnMutations.ts b/src/features/import/hooks/useColumnMutations.ts new file mode 100644 index 0000000000..6c662d1afd --- /dev/null +++ b/src/features/import/hooks/useColumnMutations.ts @@ -0,0 +1,13 @@ +import { Column } from '../utils/types'; +import { updateColumn } from '../store'; +import { useAppDispatch } from 'core/hooks'; + +export default function useColumnMutations() { + const dispatch = useAppDispatch(); + + return { + updateColumn: (index: number, column: Column) => { + dispatch(updateColumn([index, column])); + }, + }; +} diff --git a/src/features/import/hooks/useColumnOptions.ts b/src/features/import/hooks/useColumnOptions.ts new file mode 100644 index 0000000000..d3f1f4ab37 --- /dev/null +++ b/src/features/import/hooks/useColumnOptions.ts @@ -0,0 +1,50 @@ +import globalMessageIds from 'core/i18n/globalMessageIds'; +import messageIds from '../l10n/messageIds'; +import { NATIVE_PERSON_FIELDS } from 'features/views/components/types'; +import useCustomFields from 'features/profile/hooks/useCustomFields'; +import { useMessages } from 'core/i18n'; + +interface Option { + value: string; + label: string; +} + +export default function useColumnOptions(orgId: number): Option[] { + const globalMessages = useMessages(globalMessageIds); + const messages = useMessages(messageIds); + const customFields = useCustomFields(orgId).data ?? []; + + const options: Option[] = []; + + options.push({ + label: messages.configuration.mapping.id(), + value: 'id', + }); + + Object.values(NATIVE_PERSON_FIELDS).forEach((fieldSlug) => { + if (fieldSlug != 'id' && fieldSlug != 'ext_id') { + options.push({ + label: globalMessages.personFields[fieldSlug](), + value: `field:${fieldSlug}`, + }); + } + }); + + customFields.forEach((field) => + options.push({ + label: field.title, + value: `field:${field.slug}`, + }) + ); + + options.push({ + label: messages.configuration.mapping.tags(), + value: 'tag', + }); + options.push({ + label: messages.configuration.mapping.organization(), + value: 'org', + }); + + return options; +} diff --git a/src/features/import/hooks/useImportedFile.ts b/src/features/import/hooks/useImportedFile.ts index 2a6ac2d30f..456a327a76 100644 --- a/src/features/import/hooks/useImportedFile.ts +++ b/src/features/import/hooks/useImportedFile.ts @@ -1,8 +1,22 @@ import { addFile } from '../store'; +import { parseCSVFile } from '../utils/parseFile'; import { parseExcelFile } from '../utils/parseFile'; import { useAppDispatch } from 'core/hooks'; import { useState } from 'react'; -import { ImportedFile, parseCSVFile } from '../utils/parseFile'; +import { ColumnKind, ImportedFile } from '../utils/types'; + +function fileWithColumns(file: ImportedFile): ImportedFile { + return { + ...file, + sheets: file.sheets.map((sheet) => ({ + ...sheet, + columns: sheet.rows[0].data.map(() => ({ + kind: ColumnKind.UNKNOWN, + selected: false, + })), + })), + }; +} export default function useFileImport() { const [loading, setLoading] = useState(false); @@ -16,7 +30,8 @@ export default function useFileImport() { file.type === 'application/csv' ) { const res = await parseCSVFile(file); - saveData(res); + const withColumns = fileWithColumns(res); + saveData(withColumns); setLoading(false); } else if ( file.type === 'application/vnd.ms-excel' || @@ -33,7 +48,7 @@ export default function useFileImport() { } function saveData(data: ImportedFile) { - dispatch(addFile(data)); + dispatch(addFile(fileWithColumns(data))); } return { loading, parseData }; diff --git a/src/features/import/hooks/useSelectedOptions.ts b/src/features/import/hooks/useSelectedOptions.ts new file mode 100644 index 0000000000..8d4874e811 --- /dev/null +++ b/src/features/import/hooks/useSelectedOptions.ts @@ -0,0 +1,33 @@ +import { useAppSelector } from 'core/hooks'; +import { ColumnKind, FieldColumn } from '../utils/types'; + +export default function useSelectedOptions() { + const pendingFile = useAppSelector((state) => state.import.pendingFile); + const columns = pendingFile.sheets[pendingFile.selectedSheetIndex].columns; + + const allSelectedColumns = columns.filter( + (column) => column.selected && column.kind != ColumnKind.UNKNOWN + ); + + const selectedFieldColumns: FieldColumn[] = allSelectedColumns + .filter((column) => column.kind == ColumnKind.FIELD) + .map((column) => column as FieldColumn); + + return (value: string) => { + if (value == 'org' || value == 'tag') { + return false; + } + + if (value == 'id') { + return !!allSelectedColumns.find( + (column) => column.kind == ColumnKind.ID_FIELD + ); + } + + const exists = selectedFieldColumns.find( + (column) => column.field == value.slice(6) + ); + + return !!exists; + }; +} diff --git a/src/features/import/hooks/useSheets.ts b/src/features/import/hooks/useSheets.ts new file mode 100644 index 0000000000..c635ee6017 --- /dev/null +++ b/src/features/import/hooks/useSheets.ts @@ -0,0 +1,27 @@ +import { setFirstRowIsHeaders, setSelectedSheetIndex } from '../store'; +import { useAppDispatch, useAppSelector } from 'core/hooks'; + +export default function useSheets() { + const dispatch = useAppDispatch(); + const importSlice = useAppSelector((state) => state.import); + const pendingFile = importSlice.pendingFile; + + const selectedSheet = pendingFile.sheets[pendingFile.selectedSheetIndex]; + const sheets = pendingFile.sheets; + + const updateSelectedSheetIndex = (newIndex: number) => { + dispatch(setSelectedSheetIndex(newIndex)); + }; + + const updateFirstRowIsHeaders = (firstRowIsHeaders: boolean) => { + dispatch(setFirstRowIsHeaders(firstRowIsHeaders)); + }; + + return { + firstRowIsHeaders: selectedSheet.firstRowIsHeaders, + selectedSheetIndex: pendingFile.selectedSheetIndex, + sheets, + updateFirstRowIsHeaders, + updateSelectedSheetIndex, + }; +} diff --git a/src/features/import/hooks/useUIDataColumns.tsx b/src/features/import/hooks/useUIDataColumns.tsx new file mode 100644 index 0000000000..83622ef7b8 --- /dev/null +++ b/src/features/import/hooks/useUIDataColumns.tsx @@ -0,0 +1,435 @@ +import messageIds from '../l10n/messageIds'; +import notEmpty from 'utils/notEmpty'; +import { updateColumn } from '../store'; +import useTags from 'features/tags/hooks/useTags'; +import { ZetkinTag } from 'utils/types/zetkin'; +import { CellData, Column, ColumnKind } from '../utils/types'; +import { Msg, useMessages } from 'core/i18n'; +import { useAppDispatch, useAppSelector } from 'core/hooks'; + +export type UIDataColumn = { + assignTag: (tagId: number, value: CellData) => void; + columnValuesMessage: string; + deselectOrg: (value: CellData) => void; + getAssignedTags: (value: CellData) => ZetkinTag[]; + getSelectedOrgId: (value: CellData) => number | null; + numRowsByUniqueValue: Record; + numberOfEmptyRows: number; + originalColumn: CType; + renderMappingResultsMessage: () => JSX.Element | null; + selectOrg: (orgId: number, value: CellData) => void; + showColumnValuesMessage: boolean; + showMappingResultMessage: boolean; + showNeedsConfigMessage: boolean; + title: string; + unAssignTag: (tagId: number, value: CellData) => void; + uniqueValues: (string | number)[]; + updateIdField: (field: 'ext_id' | 'id') => void; + wrongIDFormat: boolean; +}; + +export default function useUIDataColumns( + orgId: number +): UIDataColumn[] { + const messages = useMessages(messageIds); + const dispatch = useAppDispatch(); + const tagsFuture = useTags(orgId); + const tags = tagsFuture.data; + const pendingFile = useAppSelector((state) => state.import.pendingFile); + + const sheet = pendingFile.sheets[pendingFile.selectedSheetIndex]; + const originalColumns = sheet.columns; + const rows = sheet.rows; + const firstRowIsHeaders = sheet.firstRowIsHeaders; + + const uiDataColumns = originalColumns.map((originalColumn, index) => { + const rowsWithValues: (string | number)[] = []; + let numberOfEmptyRows = 0; + const cellValues = rows.map((row) => row.data[index]); + + cellValues.forEach((rowValue, index) => { + if (index == 0 && firstRowIsHeaders) { + return; + } + if (rowValue) { + rowsWithValues.push(rowValue); + } else { + numberOfEmptyRows += 1; + } + }); + + const uniqueValues = Array.from(new Set(rowsWithValues)); + + let columnValuesMessage = ''; + + if (numberOfEmptyRows > 0 && uniqueValues.length == 0) { + columnValuesMessage = messages.configuration.mapping.messages.onlyEmpty({ + numEmpty: numberOfEmptyRows, + }); + } else if (numberOfEmptyRows > 0 && uniqueValues.length == 1) { + columnValuesMessage = + messages.configuration.mapping.messages.oneValueAndEmpty({ + firstValue: uniqueValues[0], + numEmpty: numberOfEmptyRows, + }); + } else if (numberOfEmptyRows > 0 && uniqueValues.length == 2) { + columnValuesMessage = + messages.configuration.mapping.messages.twoValuesAndEmpty({ + firstValue: uniqueValues[0], + numEmpty: numberOfEmptyRows, + secondValue: uniqueValues[1], + }); + } else if (numberOfEmptyRows > 0 && uniqueValues.length == 3) { + columnValuesMessage = + messages.configuration.mapping.messages.threeValuesAndEmpty({ + firstValue: uniqueValues[0], + numEmpty: numberOfEmptyRows, + secondValue: uniqueValues[1], + thirdValue: uniqueValues[2], + }); + } else if (numberOfEmptyRows > 0 && uniqueValues.length > 3) { + columnValuesMessage = + messages.configuration.mapping.messages.manyValuesAndEmpty({ + firstValue: uniqueValues[0], + numEmpty: numberOfEmptyRows, + numMoreValues: uniqueValues.length - 3, + secondValue: uniqueValues[1], + thirdValue: uniqueValues[2], + }); + } else if (numberOfEmptyRows == 0 && uniqueValues.length == 1) { + columnValuesMessage = + messages.configuration.mapping.messages.oneValueNoEmpty({ + firstValue: uniqueValues[0], + }); + } else if (numberOfEmptyRows == 0 && uniqueValues.length == 2) { + columnValuesMessage = + messages.configuration.mapping.messages.twoValuesNoEmpty({ + firstValue: uniqueValues[0], + secondValue: uniqueValues[1], + }); + } else if (numberOfEmptyRows == 0 && uniqueValues.length == 3) { + columnValuesMessage = + messages.configuration.mapping.messages.threeValuesNoEmpty({ + firstValue: uniqueValues[0], + secondValue: uniqueValues[1], + thirdValue: uniqueValues[2], + }); + } else if (numberOfEmptyRows == 0 && uniqueValues.length > 3) { + columnValuesMessage = + messages.configuration.mapping.messages.manyValuesNoEmpty({ + firstValue: uniqueValues[0], + numMoreValues: uniqueValues.length - 3, + secondValue: uniqueValues[1], + thirdValue: uniqueValues[2], + }); + } + + const valueInFirstRow = cellValues[0]; + const title = + firstRowIsHeaders && valueInFirstRow != null + ? valueInFirstRow.toString() + : messages.configuration.mapping.defaultColumnHeader({ + columnIndex: index, + }); + + const renderMappingResultsMessage = () => { + if (originalColumn.kind == ColumnKind.TAG) { + let tagIds: number[] = []; + let numRows = 0; + originalColumn.mapping.forEach((map) => { + tagIds = tagIds.concat(map.tagIds); + if (map.value) { + numRows += numRowsByUniqueValue[map.value]; + } + if (!map.value) { + numRows += numberOfEmptyRows; + } + }); + + return ( + + ); + } + + if (originalColumn.kind == ColumnKind.ID_FIELD) { + if (!originalColumn.idField) { + return null; + } + + return ( + + ); + } + + if (originalColumn.kind == ColumnKind.ORGANIZATION) { + let orgs: number[] = []; + let numPeople = 0; + originalColumn.mapping.forEach((map) => { + if (map.orgId) { + orgs = orgs.concat(map.orgId); + } + if (map.value) { + numPeople += numRowsByUniqueValue[map.value]; + } + if (!map.value) { + numPeople += numberOfEmptyRows; + } + }); + + return ( + + ); + } + + return null; + }; + + const assignTag = (tagId: number, value: CellData) => { + if (originalColumn.kind == ColumnKind.TAG && tags != null) { + const map = originalColumn.mapping.find((map) => map.value == value); + + if (!map) { + const newMap = { tagIds: [tagId], value: value }; + dispatch( + updateColumn([ + index, + { + ...originalColumn, + mapping: [...originalColumn.mapping, newMap], + }, + ]) + ); + } else { + const filteredMapping = originalColumn.mapping.filter( + (m) => m.value != value + ); + const updatedTagIds = map.tagIds.concat([tagId]); + const updatedMap = { ...map, tagIds: updatedTagIds }; + + dispatch( + updateColumn([ + index, + { + ...originalColumn, + mapping: filteredMapping.concat(updatedMap), + }, + ]) + ); + } + } + }; + + const unAssignTag = (tagId: number, value: CellData) => { + if (originalColumn.kind == ColumnKind.TAG) { + const map = originalColumn.mapping.find((map) => map.value == value); + if (map) { + const filteredMapping = originalColumn.mapping.filter( + (m) => m.value != value + ); + const updatedTagIds = map.tagIds.filter((t) => t != tagId); + + if (updatedTagIds.length == 0) { + dispatch( + updateColumn([ + index, + { + ...originalColumn, + mapping: filteredMapping, + }, + ]) + ); + } else { + const updatedMap = { ...map, tagIds: updatedTagIds }; + + dispatch( + updateColumn([ + index, + { + ...originalColumn, + mapping: filteredMapping.concat(updatedMap), + }, + ]) + ); + } + } + } + }; + + const getAssignedTags = (value: CellData): ZetkinTag[] => { + if (originalColumn.kind == ColumnKind.TAG && tags != null) { + const map = originalColumn.mapping.find((m) => m.value === value); + const assignedTags = map?.tagIds + .map((tagId) => tags.find((tag) => tag.id == tagId)) + .filter(notEmpty); + return assignedTags || []; + } + return []; + }; + + const updateIdField = (idField: 'ext_id' | 'id') => { + if (originalColumn.kind == ColumnKind.ID_FIELD) { + dispatch( + updateColumn([index, { ...originalColumn, idField: idField }]) + ); + } + }; + + const valuesAreValidZetkinIDs = cellValues.every((value, index) => { + if (index == 0 && firstRowIsHeaders) { + return true; + } + + if (!value) { + return false; + } + const stringValue = value.toString(); + const parsedToNumber = Number(stringValue); + + if (isNaN(parsedToNumber)) { + return false; + } else { + return true; + } + }); + + const wrongIDFormat = + !valuesAreValidZetkinIDs && + originalColumn.kind == ColumnKind.ID_FIELD && + originalColumn.idField == 'id'; + + const isConfigurable = [ + ColumnKind.ID_FIELD, + ColumnKind.ORGANIZATION, + ColumnKind.TAG, + ].includes(originalColumn.kind); + + const needsConfig = originalColumn.selected && isConfigurable; + const showColumnValuesMessage = !needsConfig; + + const showTagsConfigMessage = + originalColumn.kind == ColumnKind.TAG && + originalColumn.mapping.length == 0; + const showOrgConfigMessage = + originalColumn.kind == ColumnKind.ORGANIZATION && + originalColumn.mapping.length == 0; + const showIdConfigMessage = + originalColumn.kind == ColumnKind.ID_FIELD && + originalColumn.idField == null; + const showNeedsConfigMessage = + showTagsConfigMessage || + showOrgConfigMessage || + showIdConfigMessage || + wrongIDFormat; + const showMappingResultMessage = needsConfig && !showNeedsConfigMessage; + + const numRowsByUniqueValue: Record = {}; + uniqueValues.forEach((uniqueValue) => { + numRowsByUniqueValue[uniqueValue] = cellValues.filter( + (cellValue) => cellValue == uniqueValue + ).length; + }); + + const getSelectedOrgId = (value: CellData) => { + if (originalColumn.kind == ColumnKind.ORGANIZATION) { + const map = originalColumn.mapping.find((m) => m.value === value); + return map?.orgId || null; + } + return null; + }; + + const selectOrg = (orgId: number, value: CellData) => { + if (originalColumn.kind == ColumnKind.ORGANIZATION) { + const map = originalColumn.mapping.find((map) => map.value == value); + if (!map) { + const newMap = { orgId: orgId, value: value }; + dispatch( + updateColumn([ + index, + { + ...originalColumn, + mapping: [...originalColumn.mapping, newMap], + }, + ]) + ); + } else { + const filteredMapping = originalColumn.mapping.filter( + (m) => m.value != value + ); + const updatedMap = { ...map, orgId: orgId }; + + dispatch( + updateColumn([ + index, + { + ...originalColumn, + mapping: filteredMapping.concat(updatedMap), + }, + ]) + ); + } + } + }; + + const deselectOrg = (value: CellData) => { + if (originalColumn.kind == ColumnKind.ORGANIZATION) { + const map = originalColumn.mapping.find((map) => map.value == value); + if (map) { + const filteredMapping = originalColumn.mapping.filter( + (m) => m.value != value + ); + + dispatch( + updateColumn([ + index, + { + ...originalColumn, + mapping: filteredMapping, + }, + ]) + ); + } + } + }; + + return { + assignTag, + columnValuesMessage, + deselectOrg, + getAssignedTags, + getSelectedOrgId, + numRowsByUniqueValue, + numberOfEmptyRows, + originalColumn, + renderMappingResultsMessage, + selectOrg, + showColumnValuesMessage, + showMappingResultMessage, + showNeedsConfigMessage, + title, + unAssignTag, + uniqueValues, + updateIdField, + wrongIDFormat, + }; + }); + + return uiDataColumns; +} diff --git a/src/features/import/l10n/messageIds.ts b/src/features/import/l10n/messageIds.ts index 8b66caac73..05818b543d 100644 --- a/src/features/import/l10n/messageIds.ts +++ b/src/features/import/l10n/messageIds.ts @@ -4,6 +4,138 @@ import { m, makeMessages } from 'core/i18n'; export default makeMessages('feat.import', { back: m('Back'), configuration: { + configure: { + ids: { + configExplanation: m( + 'If the file you are importing is from another system than Zetkin, we can save your IDs to each person. Or, if your file is based on a sheet you exported from Zetkin, we can use the Zetkin IDs to match each row with a person in Zetkin.' + ), + externalID: m('External ID'), + externalIDExplanation: m( + 'The values in this column are IDs, but they do not come from Zetkin.' + ), + externalIDFile: m('File is from another system.'), + header: m('Configure IDs'), + wrongIDFormatWarning: m( + 'The values in this column does not look like Zetkin IDs. A Zetkin ID only contains numbers. If some cells are empty or contain f.x. letters, it can not be used as Zetkin IDs.' + ), + zetkinID: m('Zetkin ID'), + zetkinIDExplanation: m('The values in this column are Zetkin IDs.'), + zetkinIDFile: m('File is from a Zetkin export.'), + }, + orgs: { + header: m('Map values to organizations'), + organizations: m('Organization'), + status: m('Status'), + }, + tags: { + empty: m('Empty'), + header: m('Map values to tags'), + numberOfRows: m<{ numRows: number }>( + '{numRows, plural, =1 {1 row} other {# rows}}' + ), + tagsHeader: m('Tags'), + }, + }, + hide: m('Hide'), + mapping: { + configButton: m('Configure'), + defaultColumnHeader: m<{ columnIndex: number }>('Column {columnIndex}'), + emptyStateMessage: m('Start by mapping file columns.'), + fileHeader: m('File'), + finishedMappingIds: m<{ + idField: 'ext_id' | 'id'; + numValues: number; + }>( + 'Mapping {numValues, plural, =1 {1 value} other {# values}} to {idField, select, id {Zetkin ID} other {external ID}}' + ), + finishedMappingOrganizations: m<{ + numMappedTo: number; + numPeople: number; + }>( + '{numPeople, plural, =1 {1 person} other {# people}} mapped to {numMappedTo, plural, =1 {1 organization} other {# organizations}}' + ), + finishedMappingTags: m<{ + numMappedTo: number; + numRows: number; + }>( + 'Mapping {numRows, plural, =1 {1 row} other {# rows}} to {numMappedTo, plural, =1 {1 tag} other {# tags}}' + ), + header: m('Mapping'), + id: m('ID'), + mapValuesButton: m('Map values'), + messages: { + manyValuesAndEmpty: m<{ + firstValue: string | number; + numEmpty: number; + numMoreValues: number; + secondValue: string | number; + thirdValue: string | number; + }>( + '{firstValue}, {secondValue}, {thirdValue}, {numMoreValues, plural, =1 {one other value} other {# other values}} and {numEmpty, plural, =1 {one empty row} other {# empty rows}}.' + ), + manyValuesNoEmpty: m<{ + firstValue: string | number; + numMoreValues: number; + secondValue: string | number; + thirdValue: string | number; + }>( + '{firstValue}, {secondValue}, {thirdValue} and {numMoreValues, plural, =1 {one other value} other {# other values}}.' + ), + oneValueAndEmpty: m<{ firstValue: string | number; numEmpty: number }>( + '{firstValue} and {numEmpty, plural, =1 {one empty row} other {# empty rows}}.' + ), + oneValueNoEmpty: m<{ firstValue: string | number }>('{firstValue}.'), + onlyEmpty: m<{ numEmpty: number }>( + '{numEmpty, plural, =1 {one empty row} other {# empty rows}}.' + ), + + threeValuesAndEmpty: m<{ + firstValue: string | number; + numEmpty: number; + secondValue: string | number; + thirdValue: string | number; + }>( + '{firstValue}, {secondValue}, {thirdValue} and {numEmpty, plural, =1 {one empty row} other {# empty rows}}.' + ), + threeValuesNoEmpty: m<{ + firstValue: string | number; + secondValue: string | number; + thirdValue: string | number; + }>('{firstValue}, {secondValue} and {thirdValue}.'), + twoValuesAndEmpty: m<{ + firstValue: string | number; + numEmpty: number; + secondValue: string | number; + }>( + '{firstValue}, {secondValue} and {numEmpty, plural, =1 {one empty row} other {# empty rows}}.' + ), + twoValuesNoEmpty: m<{ + firstValue: string | number; + secondValue: string | number; + }>('{firstValue} and {secondValue}.'), + }, + needsConfig: m('You need to configure the IDs'), + needsMapping: m('You need to map values'), + organization: m('Organization'), + selectZetkinField: m('Import as...'), + tags: m('Tags'), + zetkinHeader: m('Zetkin'), + }, + settings: { + firstRowIsHeaders: m('First row is headers'), + header: m('Settings'), + sheetSelectHelpText: m( + 'Your file has multiple sheets, select which one to use.' + ), + sheetSelectLabel: m('Sheet'), + }, + show: m('Show'), + statusMessage: { + done: m<{ numConfiguredPeople: number }>( + 'Configures import of {numConfiguredPeople, plural, =1 {1 person} other {# people}}' + ), + notDone: m('Your configuration is incomplete'), + }, title: m('Import people'), }, done: m('Done'), diff --git a/src/features/import/store.ts b/src/features/import/store.ts index 7691554cd6..23d4671f98 100644 --- a/src/features/import/store.ts +++ b/src/features/import/store.ts @@ -1,22 +1,45 @@ -import { ImportedFile } from './utils/parseFile'; +import { Column, ImportedFile } from './utils/types'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; export interface ImportStoreSlice { - files: ImportedFile[]; + pendingFile: ImportedFile; } + const initialState: ImportStoreSlice = { - files: [], + pendingFile: { + selectedSheetIndex: 0, + sheets: [], + title: '', + }, }; + const importSlice = createSlice({ initialState, name: 'import', reducers: { addFile: (state, action: PayloadAction) => { const file = action.payload; - state.files.push(file); + state.pendingFile = file; + }, + setFirstRowIsHeaders: (state, action: PayloadAction) => { + const sheetIndex = state.pendingFile.selectedSheetIndex; + state.pendingFile.sheets[sheetIndex].firstRowIsHeaders = action.payload; + }, + setSelectedSheetIndex: (state, action: PayloadAction) => { + state.pendingFile.selectedSheetIndex = action.payload; + }, + updateColumn: (state, action: PayloadAction<[number, Column]>) => { + const [index, newColumn] = action.payload; + const sheetIndex = state.pendingFile.selectedSheetIndex; + state.pendingFile.sheets[sheetIndex].columns[index] = newColumn; }, }, }); export default importSlice; -export const { addFile } = importSlice.actions; +export const { + addFile, + setFirstRowIsHeaders, + setSelectedSheetIndex, + updateColumn, +} = importSlice.actions; diff --git a/src/features/import/utils/parseFile.ts b/src/features/import/utils/parseFile.ts index 7168b3221a..1e06155186 100644 --- a/src/features/import/utils/parseFile.ts +++ b/src/features/import/utils/parseFile.ts @@ -1,43 +1,11 @@ import * as XLSX from 'xlsx'; import { parse } from 'papaparse'; - -export type ImportedFile = { - sheets: Sheet[]; - title: string; -}; -type Sheet = { - data: Row[]; - title: string; -}; -type Row = { - data: (string | number | null)[]; -}; - -type ListMeta = { - items: { - data: (string | number | null)[]; - }; -}; - -export function createList(meta: ListMeta) { - return { - items: createListItems(meta.items.data), - }; -} - -export function createListItems(rawList: (string | number | null)[]) { - return rawList.map((i) => createListItem(i)); -} - -export function createListItem(data: string | number | null) { - return { - data, - }; -} +import { CellData, ImportedFile, Row, Sheet } from './types'; export async function parseCSVFile(file: File): Promise { return new Promise((resolve, reject) => { const rawData: ImportedFile = { + selectedSheetIndex: 0, sheets: [], title: file.name, }; @@ -49,10 +17,13 @@ export async function parseCSVFile(file: File): Promise { complete: (result) => { if (result.data) { const sheetObject = { - data: result.data, + columns: [], + currentlyConfiguring: null, + firstRowIsHeaders: true, + rows: result.data as Sheet['rows'], title: file.name, }; - rawData.sheets = [sheetObject as Sheet]; + rawData.sheets = [sheetObject]; rawData.title = file.name; } resolve(rawData); @@ -67,9 +38,18 @@ export async function parseCSVFile(file: File): Promise { }); } +interface ExcelTable { + columnList: CellData[]; + name: string; + numEmptyColumnsRemoved: number; + rows: Row[]; + useFirstRowAsHeader: boolean; +} + export async function parseExcelFile(file: File): Promise { return new Promise((resolve, reject) => { const rawData: ImportedFile = { + selectedSheetIndex: 0, sheets: [], title: file.name, }; @@ -88,24 +68,20 @@ export async function parseExcelFile(file: File): Promise { if ('!ref' in sheet && sheet['!ref'] !== undefined) { const range = XLSX.utils.decode_range(sheet['!ref']); - const table = { - columnList: createList({ - items: { - data: [], - }, - }), + const table: ExcelTable = { + columnList: [], name: name, numEmptyColumnsRemoved: 0, - rows: [] as Row[], + rows: [], useFirstRowAsHeader: false, }; for (let c = range.s.c; c <= range.e.c; c++) { - table.columnList.items.push(createListItem(null)); + table.columnList.push(null); } for (let r = range.s.r; r <= range.e.r; r++) { - const rowValues = table.columnList.items.map((col, idx) => { + const rowValues = table.columnList.map((col, idx) => { const addr = XLSX.utils.encode_cell({ c: idx, r }); const cell = sheet[addr]; return cell ? cell.d || cell.w || cell.v : undefined; @@ -118,7 +94,12 @@ export async function parseExcelFile(file: File): Promise { }); } } - rawData.sheets.push({ data: table.rows, title: table.name }); + rawData.sheets.push({ + columns: [], + firstRowIsHeaders: true, + rows: table.rows, + title: table.name, + }); } }); resolve(rawData); diff --git a/src/features/import/utils/types.ts b/src/features/import/utils/types.ts new file mode 100644 index 0000000000..60f53e68d7 --- /dev/null +++ b/src/features/import/utils/types.ts @@ -0,0 +1,67 @@ +export type CellData = string | number | null; + +export type ImportedFile = { + selectedSheetIndex: number; + sheets: Sheet[]; + title: string; +}; + +export type Sheet = { + columns: Column[]; + firstRowIsHeaders: boolean; + rows: Row[]; + title: string; +}; + +export type Row = { + data: CellData[]; +}; + +export enum ColumnKind { + FIELD = 'field', + ID_FIELD = 'id', + TAG = 'tag', + ORGANIZATION = 'org', + UNKNOWN = 'unknown', +} + +type BaseColumn = { + selected: boolean; +}; + +type UnknownColumn = BaseColumn & { + kind: ColumnKind.UNKNOWN; +}; + +export type FieldColumn = BaseColumn & { + field: string; + kind: ColumnKind.FIELD; +}; + +export type IDFieldColumn = BaseColumn & { + idField: 'ext_id' | 'id' | null; + kind: ColumnKind.ID_FIELD; +}; + +export type TagColumn = BaseColumn & { + kind: ColumnKind.TAG; + mapping: { + tagIds: number[]; + value: CellData; + }[]; +}; + +export type OrgColumn = BaseColumn & { + kind: ColumnKind.ORGANIZATION; + mapping: { + orgId: number | null; + value: CellData; + }[]; +}; + +export type Column = + | UnknownColumn + | FieldColumn + | IDFieldColumn + | TagColumn + | OrgColumn; diff --git a/src/features/tags/components/TagManager/index.tsx b/src/features/tags/components/TagManager/index.tsx index 06066250ee..1faab9d431 100644 --- a/src/features/tags/components/TagManager/index.tsx +++ b/src/features/tags/components/TagManager/index.tsx @@ -30,14 +30,12 @@ const TagManager: React.FunctionComponent = (props) => { const { updateTag } = useTagMutations(orgId); return ( - - {({ data: { tagGroupsQuery, tagsQuery } }) => { + + {({ data: { tagGroups, tags } }) => { return ( { const updated = await updateTag(newValue); diff --git a/src/features/views/components/PeopleActionButton.tsx b/src/features/views/components/PeopleActionButton.tsx index a6eff9c566..b6ceb030ba 100644 --- a/src/features/views/components/PeopleActionButton.tsx +++ b/src/features/views/components/PeopleActionButton.tsx @@ -1,21 +1,18 @@ -import { Box, Dialog, IconButton, Typography } from '@mui/material'; +import { Box } from '@mui/material'; +import { FC, useState } from 'react'; import { - Close, FolderOutlined, InsertDriveFileOutlined, UploadFileOutlined, } from '@mui/icons-material'; -import { FC, useState } from 'react'; import Importer from 'features/import/components/Importer'; -import UploadFile from 'features/import/components/UploadFile'; +import messageIds from '../l10n/messageIds'; import useCreateView from '../hooks/useCreateView'; import useFolder from '../hooks/useFolder'; import { useMessages } from 'core/i18n'; import ZUIButtonMenu from 'zui/ZUIButtonMenu'; -import messageIds from '../l10n/messageIds'; - interface PeopleActionButtonProps { folderId: number | null; orgId: number; @@ -27,7 +24,6 @@ const PeopleActionButton: FC = ({ }) => { const messages = useMessages(messageIds); const [importerDialogOpen, setImporterDialogOpen] = useState(false); - const [uploadDialogOpen, setUploadDialogOpen] = useState(false); const createView = useCreateView(orgId); const { createFolder } = useFolder(orgId, folderId); @@ -52,42 +48,16 @@ const PeopleActionButton: FC = ({ }, { icon: , - label: 'importer', //messages.actions.importPeople(), - onClick: () => setImporterDialogOpen(true), - }, - { - icon: , - label: 'upload', //messages.actions.importPeople(), - onClick: () => setUploadDialogOpen(true), + label: messages.actions.importPeople(), + onClick: () => { + setImporterDialogOpen(true); + }, }, ]} label={messages.actions.create()} /> - setUploadDialogOpen(false)} - open={uploadDialogOpen} - > - - {messages.actions.importPeople()} - - setUploadDialogOpen(false)} - sx={{ - color: (theme) => theme.palette.grey[500], - position: 'absolute', - right: 8, - top: 8, - }} - > - - - - setImporterDialogOpen(false)} - onRestart={() => setImporterDialogOpen(false)} open={importerDialogOpen} /> diff --git a/src/features/views/components/types.ts b/src/features/views/components/types.ts index d02eeea9a2..eb1a894f01 100644 --- a/src/features/views/components/types.ts +++ b/src/features/views/components/types.ts @@ -178,6 +178,7 @@ export enum NATIVE_PERSON_FIELDS { EXT_ID = 'ext_id', FIRST_NAME = 'first_name', GENDER = 'gender', + ID = 'id', LAST_NAME = 'last_name', PHONE = 'phone', STREET_ADDRESS = 'street_address', diff --git a/src/theme.ts b/src/theme.ts index 66eafc07e3..a67ac66e95 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -22,6 +22,9 @@ declare module '@mui/material/styles/createPalette' { orange: string; red: string; }; + transparentGrey: { + light: string; + }; viewColumnGallery: { blue: string; purple: string; @@ -37,7 +40,6 @@ declare module '@mui/material/styles/createPalette' { const themePalette = { background: { default: '#F9F9F9', - secondary: 'rgba(0,0,0,0.04)', }, error: { main: '#EE323E', @@ -74,6 +76,9 @@ const themePalette = { text: { secondary: 'rgba(0, 0, 0, 0.6)', }, + transparentGrey: { + light: 'rgba(0,0,0,0.04)', + }, viewColumnGallery: { blue: '#1976D2', purple: '#BA68C8', diff --git a/yarn.lock b/yarn.lock index d7e029e87c..adca43f24b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6262,7 +6262,7 @@ ccount@^2.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== -cfb@^1.1.4: +cfb@^1.1.4, cfb@~1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44" integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA== @@ -6829,7 +6829,7 @@ cpy@^8.1.2: p-filter "^2.1.0" p-map "^3.0.0" -crc-32@~1.2.0: +crc-32@~1.2.0, crc-32@~1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==