diff --git a/src/features/import/components/ImportDialog/Configure/Configuration/GenderConfig.tsx b/src/features/import/components/ImportDialog/Configure/Configuration/GenderConfig.tsx new file mode 100644 index 000000000..a6f96ce2f --- /dev/null +++ b/src/features/import/components/ImportDialog/Configure/Configuration/GenderConfig.tsx @@ -0,0 +1,65 @@ +import { FC } from 'react'; +import { Box, Divider, Typography } from '@mui/material'; + +import messageIds from 'features/import/l10n/messageIds'; +import { GenderColumn } from 'features/import/utils/types'; +import { UIDataColumn } from 'features/import/hooks/useUIDataColumn'; +import { Msg, useMessages } from 'core/i18n'; +import GenderConfigRow from './GenderConfigRow'; +import useGenderMapping from 'features/import/hooks/useGenderMapping'; + +interface GenderConfigProps { + uiDataColumn: UIDataColumn; +} + +const GenderConfig: FC = ({ uiDataColumn }) => { + const messages = useMessages(messageIds); + const { selectGender, getSelectedGender, deselectGender } = useGenderMapping( + uiDataColumn.originalColumn, + uiDataColumn.columnIndex + ); + + return ( + + + + + + + + + + {uiDataColumn.title.toLocaleUpperCase()} + + + + + {messages.configuration.configure.genders + .label() + .toLocaleUpperCase()} + + + + {uiDataColumn.uniqueValues.map((uniqueValue, index) => ( + <> + {index != 0 && } + deselectGender(uniqueValue)} + onSelectGender={(gender) => selectGender(gender, uniqueValue)} + selectedGender={getSelectedGender(uniqueValue)} + title={uniqueValue.toString()} + /> + + ))} + + ); +}; + +export default GenderConfig; diff --git a/src/features/import/components/ImportDialog/Configure/Configuration/GenderConfigRow.tsx b/src/features/import/components/ImportDialog/Configure/Configuration/GenderConfigRow.tsx new file mode 100644 index 000000000..1605afb70 --- /dev/null +++ b/src/features/import/components/ImportDialog/Configure/Configuration/GenderConfigRow.tsx @@ -0,0 +1,112 @@ +import { ArrowForward, Delete } from '@mui/icons-material'; +import { + Box, + FormControl, + IconButton, + InputLabel, + MenuItem, + Select, + Typography, +} from '@mui/material'; +import { FC } from 'react'; + +import messageIds from 'features/import/l10n/messageIds'; +import { Msg, useMessages } from 'core/i18n'; +import { Gender, genders } from '../../../../hooks/useGenderMapping'; + +interface GenderConfigRowProps { + italic?: boolean; + numRows: number; + onSelectGender: (gender: Gender | null) => void; + onDeselectGender: () => void; + selectedGender: Gender | 'unknown' | null; + title: string; +} + +const GenderConfigRow: FC = ({ + italic, + numRows, + onSelectGender, + onDeselectGender, + selectedGender, + title, +}) => { + const messages = useMessages(messageIds); + return ( + + + + + {title} + + + + + + + + + + + { + onDeselectGender(); + }} + > + + + + + + + + + ); +}; + +export default GenderConfigRow; diff --git a/src/features/import/components/ImportDialog/Configure/Configuration/index.tsx b/src/features/import/components/ImportDialog/Configure/Configuration/index.tsx index 43b449153..acac53868 100644 --- a/src/features/import/components/ImportDialog/Configure/Configuration/index.tsx +++ b/src/features/import/components/ImportDialog/Configure/Configuration/index.tsx @@ -9,6 +9,7 @@ import { ColumnKind, DateColumn, EnumColumn, + GenderColumn, IDFieldColumn, OrgColumn, TagColumn, @@ -17,6 +18,7 @@ import useUIDataColumn, { UIDataColumn, } from 'features/import/hooks/useUIDataColumn'; import EnumConfig from './EnumConfig'; +import GenderConfig from './GenderConfig'; interface ConfigurationProps { columnIndexBeingConfigured: number; @@ -49,6 +51,12 @@ const Configuration: FC = ({ uiDataColumn.originalColumn.kind == ColumnKind.ORGANIZATION && ( } /> )} + {uiDataColumn && + uiDataColumn.originalColumn.kind == ColumnKind.GENDER && ( + } + /> + )} {uiDataColumn && uiDataColumn.originalColumn.kind == ColumnKind.DATE && ( } /> )} diff --git a/src/features/import/components/ImportDialog/Configure/Mapping/FieldSelect.tsx b/src/features/import/components/ImportDialog/Configure/Mapping/FieldSelect.tsx index a467c1452..c012eb7eb 100644 --- a/src/features/import/components/ImportDialog/Configure/Mapping/FieldSelect.tsx +++ b/src/features/import/components/ImportDialog/Configure/Mapping/FieldSelect.tsx @@ -43,6 +43,10 @@ const FieldSelect: FC = ({ return `enum:${column.originalColumn.field}`; } + if (column.originalColumn.kind == ColumnKind.GENDER) { + return `field:gender`; + } + if (column.originalColumn.kind != ColumnKind.UNKNOWN) { return column.originalColumn.kind.toString(); } @@ -106,6 +110,14 @@ const FieldSelect: FC = ({ selected: true, }); onConfigureStart(); + } else if (event.target.value == 'field:gender') { + onChange({ + field: event.target.value, + kind: ColumnKind.GENDER, + mapping: [], + selected: true, + }); + onConfigureStart(); } else if (event.target.value.startsWith('field')) { onChange({ field: event.target.value.slice(6), diff --git a/src/features/import/components/ImportDialog/Configure/Mapping/MappingRow.tsx b/src/features/import/components/ImportDialog/Configure/Mapping/MappingRow.tsx index 99b97d3ae..0c2fd80b2 100644 --- a/src/features/import/components/ImportDialog/Configure/Mapping/MappingRow.tsx +++ b/src/features/import/components/ImportDialog/Configure/Mapping/MappingRow.tsx @@ -29,6 +29,7 @@ const isConfigurableColumn = (column: Column): column is ConfigurableColumn => { ColumnKind.TAG, ColumnKind.DATE, ColumnKind.ENUM, + ColumnKind.GENDER, ].includes(column.kind); }; diff --git a/src/features/import/components/ImportDialog/Configure/Preview/GenderPreview.tsx b/src/features/import/components/ImportDialog/Configure/Preview/GenderPreview.tsx new file mode 100644 index 000000000..41a5bcee1 --- /dev/null +++ b/src/features/import/components/ImportDialog/Configure/Preview/GenderPreview.tsx @@ -0,0 +1,57 @@ +import messageIds from 'features/import/l10n/messageIds'; +import PreviewGrid from './PreviewGrid'; +import { useMessages } from 'core/i18n'; +import { CellData, ColumnKind, Sheet } from 'features/import/utils/types'; + +interface GenderPreviewProps { + currentSheet: Sheet; + fieldKey: string; + fields: Record | undefined; +} + +const GenderPreview = ({ + currentSheet, + fieldKey, + fields, +}: GenderPreviewProps) => { + const messages = useMessages(messageIds); + + const map = currentSheet.columns.find( + (column) => column.kind === ColumnKind.GENDER && column.mapping.length > 0 + ); + + if (!map) { + return ( + + ); + } + + const value = fields?.[fieldKey]; + + if (value === 'o' || value === 'f' || value === 'm' || value === null) { + const key = value === null ? 'unknown' : value; + return ( + + ); + } + + // This should never happen + return ( + + ); +}; + +export default GenderPreview; diff --git a/src/features/import/components/ImportDialog/Configure/Preview/index.tsx b/src/features/import/components/ImportDialog/Configure/Preview/index.tsx index 5926051ff..35b685c54 100644 --- a/src/features/import/components/ImportDialog/Configure/Preview/index.tsx +++ b/src/features/import/components/ImportDialog/Configure/Preview/index.tsx @@ -14,6 +14,7 @@ import usePersonPreview from 'features/import/hooks/usePersonPreview'; import useSheets from 'features/import/hooks/useSheets'; import { ColumnKind, Sheet } from 'features/import/utils/types'; import EnumPreview from './EnumPreview'; +import GenderPreview from './GenderPreview'; const Preview = () => { const theme = useTheme(); @@ -155,6 +156,16 @@ const Preview = () => { /> ); } + if (column.kind === ColumnKind.GENDER) { + return ( + + ); + } } })} {orgColumnSelected && ( diff --git a/src/features/import/hooks/useGenderMapping.ts b/src/features/import/hooks/useGenderMapping.ts new file mode 100644 index 000000000..add6b3852 --- /dev/null +++ b/src/features/import/hooks/useGenderMapping.ts @@ -0,0 +1,67 @@ +import { columnUpdate } from '../store'; +import { useAppDispatch } from 'core/hooks'; +import { CellData, Column, ColumnKind } from '../utils/types'; + +export const genders = ['f', 'm', 'o'] as const; +export type Gender = typeof genders[keyof typeof genders]; + +export default function useGenderMapping(column: Column, columnIndex: number) { + const dispatch = useAppDispatch(); + + const getSelectedGender = (value: CellData) => { + if (column.kind == ColumnKind.GENDER) { + const map = column.mapping.find((m) => m.value === value); + if (!map) { + return null; + } + return map.gender ?? 'unknown'; + } + return null; + }; + + const selectGender = (gender: Gender | null, value: CellData) => { + if (column.kind !== ColumnKind.GENDER) { + return; + } + + dispatch( + columnUpdate([ + columnIndex, + { + ...column, + mapping: [ + ...column.mapping.filter((m) => m.value !== value), + { gender, value }, + ], + }, + ]) + ); + }; + + const deselectGender = (value: CellData) => { + if (column.kind != ColumnKind.GENDER) { + return; + } + + const map = column.mapping.find((map) => map.value == value); + if (map) { + const filteredMapping = column.mapping.filter((m) => m.value != value); + + dispatch( + columnUpdate([ + columnIndex, + { + ...column, + mapping: filteredMapping, + }, + ]) + ); + } + }; + + return { + deselectGender, + getSelectedGender, + selectGender, + }; +} diff --git a/src/features/import/l10n/messageIds.ts b/src/features/import/l10n/messageIds.ts index 67152d5df..969cb93b2 100644 --- a/src/features/import/l10n/messageIds.ts +++ b/src/features/import/l10n/messageIds.ts @@ -64,6 +64,15 @@ export default makeMessages('feat.import', { ), value: m('Value'), }, + genders: { + label: m('Gender'), + selectLabels: { + f: m('Female'), + m: m('Male'), + o: m('Other'), + unknown: m('Unknown'), + }, + }, ids: { externalID: m('External ID'), externalIDInfo: m( @@ -183,6 +192,7 @@ export default makeMessages('feat.import', { unfinished: { date: m('You need to configure date format'), enum: m('You need to map values'), + gender: m('You need to map values'), id: m('You need to configure the IDs'), org: m('You need to map values'), tag: m('You need to map values'), @@ -197,9 +207,16 @@ export default makeMessages('feat.import', { }, preview: { columnHeader: { + gender: m('Gender'), org: m('Organization'), tags: m('Tags'), }, + genders: { + f: m('Female'), + m: m('Male'), + o: m('Other'), + unknown: m('Unknown'), + }, next: m('Next'), noOrg: m('No organization'), noTags: m('No tags'), diff --git a/src/features/import/utils/createPreviewData.ts b/src/features/import/utils/createPreviewData.ts index a02b4d4d4..3d6a6d136 100644 --- a/src/features/import/utils/createPreviewData.ts +++ b/src/features/import/utils/createPreviewData.ts @@ -90,6 +90,20 @@ export default function createPreviewData( }; } } + + if (column.kind === ColumnKind.GENDER) { + column.mapping.forEach((mappedColumn) => { + if ( + (!mappedColumn.value && !row[colIdx]) || + mappedColumn.value === row[colIdx] + ) { + personPreviewOp.data = { + ...personPreviewOp.data, + [column.field]: mappedColumn.gender as string, + }; + } + }); + } } }); diff --git a/src/features/import/utils/prepareImportOperations.ts b/src/features/import/utils/prepareImportOperations.ts index 6343b0319..8b1165547 100644 --- a/src/features/import/utils/prepareImportOperations.ts +++ b/src/features/import/utils/prepareImportOperations.ts @@ -149,6 +149,18 @@ export default function prepareImportOperations( } } } + + if (column.kind === ColumnKind.GENDER) { + const match = column.mapping.find( + (c) => c.value === row.data[colIdx] + ); + if (match !== undefined) { + personImportOps[rowIndex].data = { + ...personImportOps[rowIndex].data, + gender: match.gender as string, + }; + } + } }); } }); diff --git a/src/features/import/utils/types.ts b/src/features/import/utils/types.ts index 8f1bc1c62..1b8664676 100644 --- a/src/features/import/utils/types.ts +++ b/src/features/import/utils/types.ts @@ -1,4 +1,5 @@ import { ZetkinPersonImportOp } from './prepareImportOperations'; +import { Gender } from '../hooks/useGenderMapping'; export type CellData = string | number | null | undefined; @@ -21,6 +22,7 @@ export type Row = { export enum ColumnKind { FIELD = 'field', + GENDER = 'gender', DATE = 'date', ID_FIELD = 'id', TAG = 'tag', @@ -48,6 +50,15 @@ export type DateColumn = BaseColumn & { kind: ColumnKind.DATE; }; +export type GenderColumn = BaseColumn & { + field: string; + kind: ColumnKind.GENDER; + mapping: { + gender: Gender | null; + value: CellData; + }[]; +}; + export type EnumColumn = BaseColumn & { field: string; kind: ColumnKind.ENUM; @@ -83,6 +94,7 @@ export type ConfigurableColumn = | IDFieldColumn | TagColumn | OrgColumn + | GenderColumn | EnumColumn; export type Column = UnknownColumn | FieldColumn | ConfigurableColumn;