From 26beb6c5fa5a4eb76abbd23c02475a4c35c309f4 Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Sat, 28 Sep 2024 12:57:32 +0200 Subject: [PATCH 01/17] Update ZetkinCustomField type and associated types to reflect new enum type. --- src/utils/types/zetkin.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/types/zetkin.ts b/src/utils/types/zetkin.ts index 784573541..e596fd7ec 100644 --- a/src/utils/types/zetkin.ts +++ b/src/utils/types/zetkin.ts @@ -179,6 +179,11 @@ export interface ZetkinPersonNativeFields { export type ZetkinPerson = ZetkinPersonNativeFields & Record | null>; +export interface EnumChoice { + key: string; + label: string; +} + export interface ZetkinCustomField { id: number; title: string; @@ -186,6 +191,7 @@ export interface ZetkinCustomField { description: string | null; type: CUSTOM_FIELD_TYPE; organization: Pick; + enum_choices: EnumChoice[]; } export interface ZetkinSession { @@ -448,6 +454,7 @@ export enum CUSTOM_FIELD_TYPE { DATE = 'date', TEXT = 'text', JSON = 'json', + ENUM_TEXT = 'enum_text', } export interface ZetkinJourney { From 04b859b15e6fbd8ae1917228590d001bdda6d494 Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Sat, 28 Sep 2024 12:58:47 +0200 Subject: [PATCH 02/17] Map enum value (key) to proper label in PersonDetailsCard --- src/features/profile/components/PersonDetailsCard.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/features/profile/components/PersonDetailsCard.tsx b/src/features/profile/components/PersonDetailsCard.tsx index 361919793..70c8dac6c 100644 --- a/src/features/profile/components/PersonDetailsCard.tsx +++ b/src/features/profile/components/PersonDetailsCard.tsx @@ -86,6 +86,11 @@ const PersonDetailsCard: React.FunctionComponent<{ value = ( {value} ); + } else if (value && field.type == 'enum_text') { + const enumItem = field.enum_choices.find((c) => c.key == value); + if (enumItem) { + value = enumItem.label; + } } return { From b0be7edd98b50f5c06fe03fd476fee5c6107bc1d Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Sun, 29 Sep 2024 09:52:32 +0200 Subject: [PATCH 03/17] Edit person enum field select box --- .../EditPersonDialog/EditPersonFields.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx b/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx index 629e30e5b..8f77d2a77 100644 --- a/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx +++ b/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx @@ -7,6 +7,7 @@ import { IconButton, InputLabel, MenuItem, + NativeSelect, Select, } from '@mui/material'; import dayjs, { Dayjs } from 'dayjs'; @@ -221,6 +222,31 @@ const EditPersonFields: FC = ({ value={fieldValues[field.slug]?.toString() ?? ''} /> ); + } else if (field.type === CUSTOM_FIELD_TYPE.ENUM_TEXT) { + return ( + + + {field.title} + + + + ); } else { return ( Date: Wed, 2 Oct 2024 23:01:56 +0200 Subject: [PATCH 04/17] Import with enum fields --- .../Configure/Configuration/EnumConfig.tsx | 95 ++++++++++++++++ .../Configure/Configuration/EnumConfigRow.tsx | 101 ++++++++++++++++++ .../Configure/Configuration/index.tsx | 5 + .../Configure/Mapping/FieldSelect.tsx | 12 +++ .../Configure/Mapping/MappingRow.tsx | 1 + .../Configure/Preview/EnumPreview.tsx | 51 +++++++++ .../ImportDialog/Configure/Preview/index.tsx | 12 +++ src/features/import/hooks/useColumn.ts | 11 +- src/features/import/hooks/useEnumMapping.ts | 93 ++++++++++++++++ src/features/import/l10n/messageIds.ts | 6 ++ .../import/utils/createPreviewData.ts | 11 ++ .../import/utils/hasUnfinishedMapping.spec.ts | 11 ++ .../import/utils/hasUnfinishedMapping.ts | 4 + .../import/utils/prepareImportOperations.ts | 12 +++ .../utils/problems/predictProblems.spec.ts | 10 ++ .../import/utils/problems/predictProblems.ts | 1 + src/features/import/utils/types.ts | 13 ++- 17 files changed, 446 insertions(+), 3 deletions(-) create mode 100644 src/features/import/components/ImportDialog/Configure/Configuration/EnumConfig.tsx create mode 100644 src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx create mode 100644 src/features/import/components/ImportDialog/Configure/Preview/EnumPreview.tsx create mode 100644 src/features/import/hooks/useEnumMapping.ts diff --git a/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfig.tsx b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfig.tsx new file mode 100644 index 000000000..b3dcdc854 --- /dev/null +++ b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfig.tsx @@ -0,0 +1,95 @@ +import { FC } from 'react'; +import { Box, Button, Divider, Typography } from '@mui/material'; + +import messageIds from 'features/import/l10n/messageIds'; +import { EnumColumn } from 'features/import/utils/types'; +import OrgConfigRow from './OrgConfigRow'; +import { UIDataColumn } from 'features/import/hooks/useUIDataColumn'; +import { useNumericRouteParams } from 'core/hooks'; +import { Msg, useMessages } from 'core/i18n'; +import useCustomFields from 'features/profile/hooks/useCustomFields'; +import useEnumMapping from 'features/import/hooks/useEnumMapping'; +import EnumConfigRow from './EnumConfigRow'; + +interface EnumConfigProps { + uiDataColumn: UIDataColumn; +} + +const EnumConfig: FC = ({ uiDataColumn }) => { + const { orgId } = useNumericRouteParams(); + const messages = useMessages(messageIds); + const { data: fields } = useCustomFields(orgId); + const { deselectOption, getSelectedOption, selectOption } = useEnumMapping( + uiDataColumn.originalColumn, + uiDataColumn.columnIndex + ); + + if (!fields || !fields.length) { + return null; + } + const field = fields.find( + (field) => field.slug === uiDataColumn.originalColumn.field + ); + const options = field?.enum_choices; + if (!options || !options.length) { + return null; + } + + return ( + + + + + + + + + + + {messages.configuration.configure.enum.value().toLocaleUpperCase()} + + + + + {messages.configuration.configure.enum.option().toLocaleUpperCase()} + + + + {uiDataColumn.uniqueValues.map((uniqueValue, index) => ( + + {index != 0 && } + deselectOption(uniqueValue)} + onSelectOption={(key) => selectOption(key, uniqueValue)} + options={options} + selectedOption={getSelectedOption(uniqueValue)} + title={uniqueValue.toString()} + /> + + ))} + {uiDataColumn.numberOfEmptyRows > 0 && ( + <> + + deselectOption(null)} + onSelectOption={(key) => selectOption(key, null)} + options={options} + selectedOption={getSelectedOption(null)} + title={messages.configuration.configure.tags.empty()} + /> + + )} + + ); +}; + +export default EnumConfig; diff --git a/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx new file mode 100644 index 000000000..ae7db3aab --- /dev/null +++ b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx @@ -0,0 +1,101 @@ +import { ArrowForward, Delete } from '@mui/icons-material'; +import { + Box, + Button, + FormControl, + IconButton, + InputLabel, + MenuItem, + Select, + Typography, +} from '@mui/material'; +import { FC, useState } from 'react'; + +import messageIds from 'features/import/l10n/messageIds'; +import { EnumChoice } from 'utils/types/zetkin'; +import { Msg, useMessages } from 'core/i18n'; + +interface EnumConfigRowProps { + italic?: boolean; + numRows: number; + onSelectOption: (key: string) => void; + onDeselectOption: () => void; + options: EnumChoice[]; + selectedOption: string | null; + title: string; +} + +const EnumConfigRow: FC = ({ + italic, + numRows, + onSelectOption, + onDeselectOption, + options, + selectedOption, + title, +}) => { + const messages = useMessages(messageIds); + const [mapping, setMapping] = useState(false); + + return ( + + + + + {title} + + + + + + + + + + + { + onDeselectOption(); + setMapping(false); + }} + > + + + + + + + + + ); +}; + +export default EnumConfigRow; diff --git a/src/features/import/components/ImportDialog/Configure/Configuration/index.tsx b/src/features/import/components/ImportDialog/Configure/Configuration/index.tsx index 617b16b0c..43b449153 100644 --- a/src/features/import/components/ImportDialog/Configure/Configuration/index.tsx +++ b/src/features/import/components/ImportDialog/Configure/Configuration/index.tsx @@ -8,6 +8,7 @@ import TagConfig from './TagConfig'; import { ColumnKind, DateColumn, + EnumColumn, IDFieldColumn, OrgColumn, TagColumn, @@ -15,6 +16,7 @@ import { import useUIDataColumn, { UIDataColumn, } from 'features/import/hooks/useUIDataColumn'; +import EnumConfig from './EnumConfig'; interface ConfigurationProps { columnIndexBeingConfigured: number; @@ -50,6 +52,9 @@ const Configuration: FC = ({ {uiDataColumn && uiDataColumn.originalColumn.kind == ColumnKind.DATE && ( } /> )} + {uiDataColumn && uiDataColumn.originalColumn.kind == ColumnKind.ENUM && ( + } /> + )} ); }; diff --git a/src/features/import/components/ImportDialog/Configure/Mapping/FieldSelect.tsx b/src/features/import/components/ImportDialog/Configure/Mapping/FieldSelect.tsx index 418c909f1..fce611642 100644 --- a/src/features/import/components/ImportDialog/Configure/Mapping/FieldSelect.tsx +++ b/src/features/import/components/ImportDialog/Configure/Mapping/FieldSelect.tsx @@ -35,6 +35,10 @@ const FieldSelect: FC = ({ return `date:${column.originalColumn.field}`; } + if (column.originalColumn.kind == ColumnKind.ENUM) { + return `enum:${column.originalColumn.field}`; + } + if (column.originalColumn.kind != ColumnKind.UNKNOWN) { return column.originalColumn.kind.toString(); } @@ -100,6 +104,14 @@ const FieldSelect: FC = ({ selected: true, }); onConfigureStart(); + } else if (event.target.value.startsWith('enum')) { + onChange({ + field: event.target.value.slice(5), + kind: ColumnKind.ENUM, + mapping: [], + selected: true, + }); + onConfigureStart(); } }} sx={{ opacity: column.originalColumn.selected ? '' : '50%' }} diff --git a/src/features/import/components/ImportDialog/Configure/Mapping/MappingRow.tsx b/src/features/import/components/ImportDialog/Configure/Mapping/MappingRow.tsx index 5843b499a..a30edf812 100644 --- a/src/features/import/components/ImportDialog/Configure/Mapping/MappingRow.tsx +++ b/src/features/import/components/ImportDialog/Configure/Mapping/MappingRow.tsx @@ -28,6 +28,7 @@ const isConfigurableColumn = (column: Column): column is ConfigurableColumn => { ColumnKind.ORGANIZATION, ColumnKind.TAG, ColumnKind.DATE, + ColumnKind.ENUM, ].includes(column.kind); }; diff --git a/src/features/import/components/ImportDialog/Configure/Preview/EnumPreview.tsx b/src/features/import/components/ImportDialog/Configure/Preview/EnumPreview.tsx new file mode 100644 index 000000000..b9dcdfcf9 --- /dev/null +++ b/src/features/import/components/ImportDialog/Configure/Preview/EnumPreview.tsx @@ -0,0 +1,51 @@ +import messageIds from 'features/import/l10n/messageIds'; +import PreviewGrid from './PreviewGrid'; +import { useMessages } from 'core/i18n'; +import { EnumChoice, ZetkinOrganization } from 'utils/types/zetkin'; +import { CellData, ColumnKind, Sheet } from 'features/import/utils/types'; +import useColumn from 'features/import/hooks/useColumn'; +import { useNumericRouteParams } from 'core/hooks'; + +interface EnumPreviewProps { + currentSheet: Sheet; + fieldKey: string; + fields: Record | undefined; +} + +const EnumPreview = ({ currentSheet, fieldKey, fields }: EnumPreviewProps) => { + const { orgId } = useNumericRouteParams(); + const messages = useMessages(messageIds); + const { fieldOptions } = useColumn(orgId); + + const hasMapped = currentSheet.columns.some( + (column) => + column.kind === ColumnKind.ENUM && + column.field === fieldKey && + column.mapping.length > 0 + ); + + let columnHeader = ''; + let enumOptions: EnumChoice[] | undefined = []; + fieldOptions.flat().forEach((columnOp) => { + if (columnOp.value === `enum:${fieldKey}`) { + columnHeader = columnOp.label; + enumOptions = columnOp.enumChoices; + } + }); + + const value = fields?.[fieldKey]; + const option = enumOptions.find((o) => o.key == value); + + return ( + + ); +}; + +export default EnumPreview; diff --git a/src/features/import/components/ImportDialog/Configure/Preview/index.tsx b/src/features/import/components/ImportDialog/Configure/Preview/index.tsx index dc1893bcb..f5040f1ff 100644 --- a/src/features/import/components/ImportDialog/Configure/Preview/index.tsx +++ b/src/features/import/components/ImportDialog/Configure/Preview/index.tsx @@ -13,6 +13,7 @@ import { useNumericRouteParams } from 'core/hooks'; 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'; const Preview = () => { const theme = useTheme(); @@ -149,6 +150,17 @@ const Preview = () => { /> ); } + + if (column.kind === ColumnKind.ENUM) { + return ( + + ); + } } })} {tagColumnSelected && ( diff --git a/src/features/import/hooks/useColumn.ts b/src/features/import/hooks/useColumn.ts index e2bcec1ea..d699602d5 100644 --- a/src/features/import/hooks/useColumn.ts +++ b/src/features/import/hooks/useColumn.ts @@ -1,5 +1,5 @@ import { columnUpdate } from '../store'; -import { CUSTOM_FIELD_TYPE } from 'utils/types/zetkin'; +import { CUSTOM_FIELD_TYPE, EnumChoice } from 'utils/types/zetkin'; import globalMessageIds from 'core/i18n/globalMessageIds'; import { NATIVE_PERSON_FIELDS } from 'features/views/components/types'; import useCustomFields from 'features/profile/hooks/useCustomFields'; @@ -10,6 +10,7 @@ import { useAppDispatch, useAppSelector } from 'core/hooks'; export interface Option { value: string; label: string; + enumChoices?: EnumChoice[]; } export default function useColumn(orgId: number) { @@ -53,7 +54,7 @@ export default function useColumn(orgId: number) { return column.field == value.slice(6); } - if (column.kind == ColumnKind.DATE) { + if (column.kind == ColumnKind.DATE || column.kind == ColumnKind.ENUM) { return column.field == value.slice(5); } }); @@ -74,6 +75,12 @@ export default function useColumn(orgId: number) { label: field.title, value: `date:${field.slug}`, }; + } else if (field.type == CUSTOM_FIELD_TYPE.ENUM_TEXT) { + return { + label: field.title, + value: `enum:${field.slug}`, + enumChoices: field.enum_choices, + }; } else { return { label: field.title, diff --git a/src/features/import/hooks/useEnumMapping.ts b/src/features/import/hooks/useEnumMapping.ts new file mode 100644 index 000000000..77c5e2da4 --- /dev/null +++ b/src/features/import/hooks/useEnumMapping.ts @@ -0,0 +1,93 @@ +import { columnUpdate } from '../store'; +import { useAppDispatch } from 'core/hooks'; +import { CellData, Column, ColumnKind } from '../utils/types'; +import { EnumChoice } from 'utils/types/zetkin'; + +export default function useEnumMapping(column: Column, columnIndex: number) { + const dispatch = useAppDispatch(); + + const getSelectedOption = (value: CellData) => { + if (column.kind == ColumnKind.ENUM) { + const map = column.mapping.find((m) => m.value === value); + return map?.key || null; + } + return null; + }; + + const selectOption = (key: string, value: CellData) => { + if (column.kind == ColumnKind.ENUM) { + // Check if there is already a map for this row value to a key + const map = column.mapping.find((map) => map.value == value); + // If no map for that value + if (!map) { + const newMap = { key: key, value: value }; + dispatch( + // Add value to mapping for the column + columnUpdate([ + columnIndex, + { + ...column, + mapping: [...column.mapping, newMap], + }, + ]) + ); + } else { + // If there is already a map, replace it + // Find mappings that are not for this row value + const filteredMapping = column.mapping.filter((m) => m.value != value); + // New key for that row value + const updatedMap = { ...map, key: key }; + + dispatch( + columnUpdate([ + columnIndex, + { + ...column, + mapping: filteredMapping.concat(updatedMap), // Add the new mapping along side existing ones + }, + ]) + ); + } + } + }; + + const selectOptions = (mapping: { key: string; value: CellData }[]) => { + if (column.kind == ColumnKind.ENUM) { + dispatch( + columnUpdate([ + columnIndex, + { + ...column, + mapping, + }, + ]) + ); + } + }; + + const deselectOption = (value: CellData) => { + if (column.kind == ColumnKind.ENUM) { + 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 { + deselectOption, + getSelectedOption, + selectOption, + selectOptions, + }; +} diff --git a/src/features/import/l10n/messageIds.ts b/src/features/import/l10n/messageIds.ts index f87e3024d..2f205bc8b 100644 --- a/src/features/import/l10n/messageIds.ts +++ b/src/features/import/l10n/messageIds.ts @@ -55,6 +55,11 @@ export default makeMessages('feat.import', { 'Some of the values in this column can not be parsed into dates using this format.' ), }, + enum: { + option: m('Option'), + header: m('Map values to options'), + value: m('Value'), + }, ids: { configExplanation: m( 'Importing with IDs allows Zetkin (now or in the future) to update existing people in the database instead of creating duplicates.' @@ -178,6 +183,7 @@ export default makeMessages('feat.import', { id: m('You need to configure the IDs'), org: m('You need to map values'), tag: m('You need to map values'), + enum: m('You need to map values'), }, zetkinFieldGroups: { fields: m('Fields'), diff --git a/src/features/import/utils/createPreviewData.ts b/src/features/import/utils/createPreviewData.ts index af0d8dd96..6a5089bc8 100644 --- a/src/features/import/utils/createPreviewData.ts +++ b/src/features/import/utils/createPreviewData.ts @@ -54,6 +54,17 @@ export default function createPreviewData( }); } + if (column.kind === ColumnKind.ENUM) { + column.mapping.forEach((mappedColumn) => { + if (mappedColumn.value === row[colIdx]) { + personPreviewOp.data = { + ...personPreviewOp.data, + [`${column.field}`]: row[colIdx], + }; + } + }); + } + if (column.kind === ColumnKind.DATE) { if (row[colIdx] && column.dateFormat) { const date = parseDate(row[colIdx], column.dateFormat); diff --git a/src/features/import/utils/hasUnfinishedMapping.spec.ts b/src/features/import/utils/hasUnfinishedMapping.spec.ts index 19697f6be..17cab79e1 100644 --- a/src/features/import/utils/hasUnfinishedMapping.spec.ts +++ b/src/features/import/utils/hasUnfinishedMapping.spec.ts @@ -50,4 +50,15 @@ describe('hasUnfinishedMapping()', () => { expect(unfinishedMapping).toBe(true); }); + + it('returns true if columnKind is ENUM and mapping length is 0', () => { + const unfinishedMapping = hasUnfinishedMapping({ + field: 'enum:dummyField', + kind: ColumnKind.ENUM, + mapping: [], + selected: true, + }); + + expect(unfinishedMapping).toBe(true); + }); }); diff --git a/src/features/import/utils/hasUnfinishedMapping.ts b/src/features/import/utils/hasUnfinishedMapping.ts index dff98aa4c..02464c758 100644 --- a/src/features/import/utils/hasUnfinishedMapping.ts +++ b/src/features/import/utils/hasUnfinishedMapping.ts @@ -21,6 +21,10 @@ export default function hasUnfinishedMapping(column: Column) { return column.dateFormat === null; } + if (column.kind === ColumnKind.ENUM) { + return column.mapping.length === 0; + } + //Column kind must be ORGANIZATION return column.mapping.length === 0; } diff --git a/src/features/import/utils/prepareImportOperations.ts b/src/features/import/utils/prepareImportOperations.ts index 665243194..641f6ae1e 100644 --- a/src/features/import/utils/prepareImportOperations.ts +++ b/src/features/import/utils/prepareImportOperations.ts @@ -3,6 +3,7 @@ import { CountryCode, parsePhoneNumber } from 'libphonenumber-js'; import getUniqueTags from './getUniqueTags'; import parseDate from './parseDate'; import { CellData, ColumnKind, Sheet } from './types'; +import { valueToPercent } from '@mui/base'; export type ZetkinPersonImportOp = { data?: Record; @@ -109,6 +110,17 @@ export default function prepareImportOperations( }); } + if (column.kind === ColumnKind.ENUM) { + column.mapping.forEach((mappedColumn) => { + if (mappedColumn.value === row.data[colIdx] && mappedColumn.key) { + personImportOps[rowIndex].data = { + ...personImportOps[rowIndex].data, + [`${column.field}`]: mappedColumn.key, + }; + } + }); + } + if (column.kind === ColumnKind.DATE) { if (column.dateFormat) { const fieldKey = column.field; diff --git a/src/features/import/utils/problems/predictProblems.spec.ts b/src/features/import/utils/problems/predictProblems.spec.ts index bcf79e791..874913c4c 100644 --- a/src/features/import/utils/problems/predictProblems.spec.ts +++ b/src/features/import/utils/problems/predictProblems.spec.ts @@ -26,6 +26,16 @@ function makeFullField( slug: 'field', title: 'Field', type: CUSTOM_FIELD_TYPE.TEXT, + enum_choices: [ + { + key: 'first', + label: 'First', + }, + { + key: 'second', + label: 'Second', + }, + ], ...overrides, }; } diff --git a/src/features/import/utils/problems/predictProblems.ts b/src/features/import/utils/problems/predictProblems.ts index 96bec2c9a..181015d3f 100644 --- a/src/features/import/utils/problems/predictProblems.ts +++ b/src/features/import/utils/problems/predictProblems.ts @@ -20,6 +20,7 @@ const VALIDATORS: Record boolean> = { return false; } }, + enum_text: () => true, json: () => true, text: () => true, url: (value) => isURL(value), diff --git a/src/features/import/utils/types.ts b/src/features/import/utils/types.ts index 1ecd78c22..5255874f0 100644 --- a/src/features/import/utils/types.ts +++ b/src/features/import/utils/types.ts @@ -25,6 +25,7 @@ export enum ColumnKind { ID_FIELD = 'id', TAG = 'tag', ORGANIZATION = 'org', + ENUM = 'enum', UNKNOWN = 'unknown', } @@ -47,6 +48,15 @@ export type DateColumn = BaseColumn & { kind: ColumnKind.DATE; }; +export type EnumColumn = BaseColumn & { + field: string; + kind: ColumnKind.ENUM; + mapping: { + key: string; + value: CellData; + }[]; +}; + export type IDFieldColumn = BaseColumn & { idField: 'ext_id' | 'id' | null; kind: ColumnKind.ID_FIELD; @@ -72,7 +82,8 @@ export type ConfigurableColumn = | DateColumn | IDFieldColumn | TagColumn - | OrgColumn; + | OrgColumn + | EnumColumn; export type Column = UnknownColumn | FieldColumn | ConfigurableColumn; From a2ffcdb78fdc312d05676c831fd8eb902df484d6 Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Wed, 2 Oct 2024 23:17:27 +0200 Subject: [PATCH 05/17] Fix code formatting. --- .../Configure/Configuration/EnumConfig.tsx | 3 +-- .../Configure/Configuration/EnumConfigRow.tsx | 9 +++------ .../Configure/Preview/EnumPreview.tsx | 2 +- .../ImportDialog/Configure/Preview/index.tsx | 2 +- src/features/import/hooks/useColumn.ts | 2 +- src/features/import/hooks/useEnumMapping.ts | 1 - src/features/import/l10n/messageIds.ts | 4 ++-- .../import/utils/prepareImportOperations.ts | 1 - .../utils/problems/predictProblems.spec.ts | 16 ++++++++-------- .../EditPersonDialog/EditPersonFields.tsx | 5 ++--- 10 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfig.tsx b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfig.tsx index b3dcdc854..d71cb59c7 100644 --- a/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfig.tsx +++ b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfig.tsx @@ -1,9 +1,8 @@ import { FC } from 'react'; -import { Box, Button, Divider, Typography } from '@mui/material'; +import { Box, Divider, Typography } from '@mui/material'; import messageIds from 'features/import/l10n/messageIds'; import { EnumColumn } from 'features/import/utils/types'; -import OrgConfigRow from './OrgConfigRow'; import { UIDataColumn } from 'features/import/hooks/useUIDataColumn'; import { useNumericRouteParams } from 'core/hooks'; import { Msg, useMessages } from 'core/i18n'; diff --git a/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx index ae7db3aab..4a616300c 100644 --- a/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx +++ b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx @@ -1,7 +1,6 @@ import { ArrowForward, Delete } from '@mui/icons-material'; import { Box, - Button, FormControl, IconButton, InputLabel, @@ -9,7 +8,7 @@ import { Select, Typography, } from '@mui/material'; -import { FC, useState } from 'react'; +import { FC } from 'react'; import messageIds from 'features/import/l10n/messageIds'; import { EnumChoice } from 'utils/types/zetkin'; @@ -35,7 +34,6 @@ const EnumConfigRow: FC = ({ title, }) => { const messages = useMessages(messageIds); - const [mapping, setMapping] = useState(false); return ( @@ -60,10 +58,10 @@ const EnumConfigRow: FC = ({ > - + { onChange(field.slug, ev.target.value); }} - label={field.title} + value={fieldValues[field.slug]?.toString() ?? ''} > None From d326a04831a5859b347b4af91e268889ae7fb595 Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Fri, 4 Oct 2024 00:22:24 +0200 Subject: [PATCH 06/17] Add enum_text smart search --- .../Configure/Configuration/EnumConfig.tsx | 2 +- src/features/import/hooks/useColumn.ts | 2 +- .../EditPersonDialog/EditPersonFields.tsx | 5 +- .../components/PersonDetailsCard.stories.tsx | 3 + .../PersonField/DisplayPersonField.tsx | 70 +++++++++++++------ .../components/filters/PersonField/index.tsx | 28 +++++++- src/features/smartSearch/l10n/messageIds.ts | 8 +++ src/utils/types/zetkin.ts | 2 +- 8 files changed, 95 insertions(+), 25 deletions(-) diff --git a/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfig.tsx b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfig.tsx index d71cb59c7..9a060d6c2 100644 --- a/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfig.tsx +++ b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfig.tsx @@ -56,7 +56,7 @@ const EnumConfig: FC = ({ uiDataColumn }) => { - {messages.configuration.configure.enum.option().toLocaleUpperCase()} + {messages.configuration.configure.enum.none().toLocaleUpperCase()} diff --git a/src/features/import/hooks/useColumn.ts b/src/features/import/hooks/useColumn.ts index 4d660cf29..3dc9582f0 100644 --- a/src/features/import/hooks/useColumn.ts +++ b/src/features/import/hooks/useColumn.ts @@ -77,7 +77,7 @@ export default function useColumn(orgId: number) { }; } else if (field.type == CUSTOM_FIELD_TYPE.ENUM_TEXT) { return { - enumChoices: field.enum_choices, + enumChoices: field.enum_choices || null, label: field.title, value: `enum:${field.slug}`, }; diff --git a/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx b/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx index 7859e0909..c8d4fec13 100644 --- a/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx +++ b/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx @@ -221,7 +221,10 @@ const EditPersonFields: FC = ({ value={fieldValues[field.slug]?.toString() ?? ''} /> ); - } else if (field.type === CUSTOM_FIELD_TYPE.ENUM_TEXT) { + } else if ( + field.type === CUSTOM_FIELD_TYPE.ENUM_TEXT && + field.enum_choices + ) { return ( diff --git a/src/features/profile/components/PersonDetailsCard.stories.tsx b/src/features/profile/components/PersonDetailsCard.stories.tsx index d5d6272a2..47ce90e3c 100644 --- a/src/features/profile/components/PersonDetailsCard.stories.tsx +++ b/src/features/profile/components/PersonDetailsCard.stories.tsx @@ -25,6 +25,7 @@ Primary.args = { customFields: [ { description: null, + enum_choices: null, id: 1, organization: mockOrganization(), slug: 'birthday', @@ -33,6 +34,7 @@ Primary.args = { }, { description: null, + enum_choices: null, id: 1, organization: mockOrganization(), slug: 'facebook', @@ -41,6 +43,7 @@ Primary.args = { }, { description: null, + enum_choices: null, id: 1, organization: mockOrganization(), slug: 'twitter', diff --git a/src/features/smartSearch/components/filters/PersonField/DisplayPersonField.tsx b/src/features/smartSearch/components/filters/PersonField/DisplayPersonField.tsx index 270feb515..1f62cc80c 100644 --- a/src/features/smartSearch/components/filters/PersonField/DisplayPersonField.tsx +++ b/src/features/smartSearch/components/filters/PersonField/DisplayPersonField.tsx @@ -34,34 +34,64 @@ const DisplayPersonField = ({ const field = getField(slug); const fieldType = field?.type || ''; - if (fieldType != 'date' && fieldType != 'text' && fieldType != 'url') { + if ( + fieldType != 'date' && + fieldType != 'text' && + fieldType != 'url' && + fieldType != 'enum_text' + ) { // TODO: return null; } - + let fieldMessage; + if (fieldType == 'date') { + fieldMessage = ( + , + timeFrame: , + }} + /> + ); + } else if ( + fieldType == 'enum_text' && + field?.enum_choices && + search !== undefined + ) { + fieldMessage = ( + , + searchTerm: ( + c.key == search)?.label || + search + } + /> + ), + }} + /> + ); + } else { + fieldMessage = ( + , + searchTerm: , + }} + /> + ); + } return ( , - field: - fieldType == 'date' ? ( - , - timeFrame: , - }} - /> - ) : ( - , - searchTerm: , - }} - /> - ), + field: fieldMessage, }} /> ); diff --git a/src/features/smartSearch/components/filters/PersonField/index.tsx b/src/features/smartSearch/components/filters/PersonField/index.tsx index 386e9368d..b3b807fc1 100644 --- a/src/features/smartSearch/components/filters/PersonField/index.tsx +++ b/src/features/smartSearch/components/filters/PersonField/index.tsx @@ -1,4 +1,4 @@ -import { MenuItem } from '@mui/material'; +import { MenuItem, Select } from '@mui/material'; import { FormEvent, useEffect } from 'react'; import { CUSTOM_FIELD_TYPE } from 'utils/types/zetkin'; @@ -181,6 +181,32 @@ const PersonField = ({ }} /> ); + } else if ( + type == CUSTOM_FIELD_TYPE.ENUM_TEXT && + selectedField?.enum_choices + ) { + return ( + { + handleValueChange(e.target.value); + }} + value={filter.config.search || ''} + > + {selectedField.enum_choices.map((c) => ( + + {c.label} + + ))} + + ), + }} + /> + ); } else { return ( ( '{fieldSelect} is {timeFrame}' ), + enum_text: m<{ + fieldSelect: ReactElement; + selectInput: ReactElement; + }>('{fieldSelect} is "{selectInput}"'), none: m("This organization doesn't have any custom fields yet."), text: m<{ fieldSelect: ReactElement; freeTextInput: ReactElement }>( '{fieldSelect} matches "{freeTextInput}"' @@ -420,6 +424,10 @@ export default makeMessages('feat.smartSearch', { date: m<{ fieldName: ReactElement | string; timeFrame: ReactElement }>( '{fieldName} is {timeFrame}' ), + enum_text: m<{ + fieldName: ReactElement | string; + searchTerm: ReactElement | string; + }>('{fieldName} is "{searchTerm}"'), text: m<{ fieldName: ReactElement | string; searchTerm: ReactElement | string; diff --git a/src/utils/types/zetkin.ts b/src/utils/types/zetkin.ts index e596fd7ec..c02c17e4b 100644 --- a/src/utils/types/zetkin.ts +++ b/src/utils/types/zetkin.ts @@ -191,7 +191,7 @@ export interface ZetkinCustomField { description: string | null; type: CUSTOM_FIELD_TYPE; organization: Pick; - enum_choices: EnumChoice[]; + enum_choices?: EnumChoice[] | null; } export interface ZetkinSession { From 0a1d38966ce65de666adda7d1ddc2bccd15a7500 Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Fri, 4 Oct 2024 00:29:10 +0200 Subject: [PATCH 07/17] Add missing type fixes --- src/features/import/hooks/useColumn.ts | 7 +++++-- src/features/profile/components/PersonDetailsCard.tsx | 2 +- .../smartSearch/components/filters/PersonField/index.tsx | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/features/import/hooks/useColumn.ts b/src/features/import/hooks/useColumn.ts index 3dc9582f0..d5e4c043b 100644 --- a/src/features/import/hooks/useColumn.ts +++ b/src/features/import/hooks/useColumn.ts @@ -75,9 +75,12 @@ export default function useColumn(orgId: number) { label: field.title, value: `date:${field.slug}`, }; - } else if (field.type == CUSTOM_FIELD_TYPE.ENUM_TEXT) { + } else if ( + field.type == CUSTOM_FIELD_TYPE.ENUM_TEXT && + field.enum_choices + ) { return { - enumChoices: field.enum_choices || null, + enumChoices: field.enum_choices, label: field.title, value: `enum:${field.slug}`, }; diff --git a/src/features/profile/components/PersonDetailsCard.tsx b/src/features/profile/components/PersonDetailsCard.tsx index 70c8dac6c..7afa1f2b7 100644 --- a/src/features/profile/components/PersonDetailsCard.tsx +++ b/src/features/profile/components/PersonDetailsCard.tsx @@ -86,7 +86,7 @@ const PersonDetailsCard: React.FunctionComponent<{ value = ( {value} ); - } else if (value && field.type == 'enum_text') { + } else if (value && field.type == 'enum_text' && field.enum_choices) { const enumItem = field.enum_choices.find((c) => c.key == value); if (enumItem) { value = enumItem.label; diff --git a/src/features/smartSearch/components/filters/PersonField/index.tsx b/src/features/smartSearch/components/filters/PersonField/index.tsx index b3b807fc1..e6c7b978e 100644 --- a/src/features/smartSearch/components/filters/PersonField/index.tsx +++ b/src/features/smartSearch/components/filters/PersonField/index.tsx @@ -1,4 +1,4 @@ -import { MenuItem, Select } from '@mui/material'; +import { MenuItem } from '@mui/material'; import { FormEvent, useEffect } from 'react'; import { CUSTOM_FIELD_TYPE } from 'utils/types/zetkin'; From 436715089c83ce768f650e9315a097ff521836f9 Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Tue, 15 Oct 2024 16:30:37 +0200 Subject: [PATCH 08/17] Add mapping of enum choice keys to labels in view column. SimpleColumn renamed to PersonFieldColumn, it was only used by PersonField. --- .../columnTypes/PersonFieldColumnType.tsx | 42 +++++++++++++++++++ .../columnTypes/SimpleColumnType.tsx | 22 ---------- .../ViewDataTable/columnTypes/index.ts | 4 +- src/features/views/components/types.ts | 3 +- 4 files changed, 46 insertions(+), 25 deletions(-) create mode 100644 src/features/views/components/ViewDataTable/columnTypes/PersonFieldColumnType.tsx delete mode 100644 src/features/views/components/ViewDataTable/columnTypes/SimpleColumnType.tsx diff --git a/src/features/views/components/ViewDataTable/columnTypes/PersonFieldColumnType.tsx b/src/features/views/components/ViewDataTable/columnTypes/PersonFieldColumnType.tsx new file mode 100644 index 000000000..7b61753a3 --- /dev/null +++ b/src/features/views/components/ViewDataTable/columnTypes/PersonFieldColumnType.tsx @@ -0,0 +1,42 @@ +import { GridColDef } from '@mui/x-data-grid-pro'; + +import { IColumnType } from '.'; +import { PersonFieldViewColumn, ZetkinViewColumn } from '../../types'; +import { EnumChoice } from 'utils/types/zetkin'; + +type SimpleData = string | number | boolean | null; + +export default class PersonFieldColumnType + implements IColumnType +{ + cellToString(): string { + return ''; + } + + private enumChoices: EnumChoice[] | null = null; + + getColDef(column: PersonFieldViewColumn): Omit { + return { + filterable: true, + valueGetter: (params) => { + const cell = params.row[params.field]; + if (column.config.enum_choices) { + this.enumChoices = column.config.enum_choices; + const choice = column.config.enum_choices.find((c) => c.key == cell); + return choice?.label ?? ''; + } else { + return cell ? cell.toString() : ''; + } + }, + }; + } + + getSearchableStrings(cell: SimpleData): string[] { + if (this.enumChoices) { + const choice = this.enumChoices.find((c) => c.key == cell); + return choice ? [choice.label] : []; + } else { + return cell && cell !== true ? [cell.toString()] : []; + } + } +} diff --git a/src/features/views/components/ViewDataTable/columnTypes/SimpleColumnType.tsx b/src/features/views/components/ViewDataTable/columnTypes/SimpleColumnType.tsx deleted file mode 100644 index 82d496ec5..000000000 --- a/src/features/views/components/ViewDataTable/columnTypes/SimpleColumnType.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { GridColDef } from '@mui/x-data-grid-pro'; - -import { IColumnType } from '.'; -import { ZetkinViewColumn } from '../../types'; - -type SimpleData = string | number | boolean | null; - -export default class SimpleColumnType - implements IColumnType -{ - cellToString(cell: SimpleData | null): string { - return cell ? cell.toString() : ''; - } - - getColDef(): Omit { - return {}; - } - - getSearchableStrings(cell: SimpleData): string[] { - return cell && cell !== true ? [cell.toString()] : []; - } -} diff --git a/src/features/views/components/ViewDataTable/columnTypes/index.ts b/src/features/views/components/ViewDataTable/columnTypes/index.ts index a2eeac4e0..f31bcb94c 100644 --- a/src/features/views/components/ViewDataTable/columnTypes/index.ts +++ b/src/features/views/components/ViewDataTable/columnTypes/index.ts @@ -8,7 +8,7 @@ import LocalQueryColumnType from './LocalQueryColumnType'; import LocalTextColumnType from './LocalTextColumnType'; import OrganizerActionColumnType from './OrganizerActionColumnType'; import PersonTagColumnType from './PersonTagColumnType'; -import SimpleColumnType from './SimpleColumnType'; +import PersonFieldColumnType from './PersonFieldColumnType'; import SurveyOptionColumnType from './SurveyOptionColumnType'; import SurveyOptionsColumnType from './SurveyOptionsColumnType'; import SurveyResponseColumnType from './SurveyResponseColumnType'; @@ -95,7 +95,7 @@ const columnTypes: Record = { [COLUMN_TYPE.LOCAL_PERSON]: new LocalPersonColumnType(), [COLUMN_TYPE.LOCAL_QUERY]: new LocalQueryColumnType(), [COLUMN_TYPE.ORGANIZER_ACTION]: new OrganizerActionColumnType(), - [COLUMN_TYPE.PERSON_FIELD]: new SimpleColumnType(), + [COLUMN_TYPE.PERSON_FIELD]: new PersonFieldColumnType(), [COLUMN_TYPE.PERSON_QUERY]: new LocalQueryColumnType(), [COLUMN_TYPE.PERSON_TAG]: new PersonTagColumnType(), [COLUMN_TYPE.SURVEY_OPTION]: new SurveyOptionColumnType(), diff --git a/src/features/views/components/types.ts b/src/features/views/components/types.ts index eb1a894f0..dfb824683 100644 --- a/src/features/views/components/types.ts +++ b/src/features/views/components/types.ts @@ -1,4 +1,4 @@ -import { ZetkinOrganization } from 'utils/types/zetkin'; +import { EnumChoice, ZetkinOrganization } from 'utils/types/zetkin'; import { AnyFilterConfig, ZetkinQuery, @@ -93,6 +93,7 @@ export interface OrganizerActionViewColumn extends ZetkinViewColumnBase { export interface PersonFieldViewColumn extends ZetkinViewColumnBase { type: COLUMN_TYPE.PERSON_FIELD; config: { + enum_choices?: EnumChoice[]; field: string; }; } From 1d0dffb73e2c05de2f379a7ef0e090c3f4d7a1a0 Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Thu, 17 Oct 2024 16:19:49 +0200 Subject: [PATCH 09/17] Update enum_text to enum --- src/features/import/hooks/useColumn.ts | 2 +- src/features/import/utils/problems/predictProblems.ts | 2 +- .../profile/components/EditPersonDialog/EditPersonFields.tsx | 2 +- src/features/profile/components/PersonDetailsCard.tsx | 2 +- .../components/filters/PersonField/DisplayPersonField.tsx | 4 ++-- .../smartSearch/components/filters/PersonField/index.tsx | 4 ++-- src/features/smartSearch/l10n/messageIds.ts | 4 ++-- src/utils/types/zetkin.ts | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/features/import/hooks/useColumn.ts b/src/features/import/hooks/useColumn.ts index d5e4c043b..1addc4493 100644 --- a/src/features/import/hooks/useColumn.ts +++ b/src/features/import/hooks/useColumn.ts @@ -76,7 +76,7 @@ export default function useColumn(orgId: number) { value: `date:${field.slug}`, }; } else if ( - field.type == CUSTOM_FIELD_TYPE.ENUM_TEXT && + field.type == CUSTOM_FIELD_TYPE.ENUM && field.enum_choices ) { return { diff --git a/src/features/import/utils/problems/predictProblems.ts b/src/features/import/utils/problems/predictProblems.ts index 181015d3f..33973e1d8 100644 --- a/src/features/import/utils/problems/predictProblems.ts +++ b/src/features/import/utils/problems/predictProblems.ts @@ -20,7 +20,7 @@ const VALIDATORS: Record boolean> = { return false; } }, - enum_text: () => true, + enum: () => true, json: () => true, text: () => true, url: (value) => isURL(value), diff --git a/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx b/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx index c8d4fec13..ff330f3a3 100644 --- a/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx +++ b/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx @@ -222,7 +222,7 @@ const EditPersonFields: FC = ({ /> ); } else if ( - field.type === CUSTOM_FIELD_TYPE.ENUM_TEXT && + field.type === CUSTOM_FIELD_TYPE.ENUM && field.enum_choices ) { return ( diff --git a/src/features/profile/components/PersonDetailsCard.tsx b/src/features/profile/components/PersonDetailsCard.tsx index 7afa1f2b7..110ad69f2 100644 --- a/src/features/profile/components/PersonDetailsCard.tsx +++ b/src/features/profile/components/PersonDetailsCard.tsx @@ -86,7 +86,7 @@ const PersonDetailsCard: React.FunctionComponent<{ value = ( {value} ); - } else if (value && field.type == 'enum_text' && field.enum_choices) { + } else if (value && field.type == 'enum' && field.enum_choices) { const enumItem = field.enum_choices.find((c) => c.key == value); if (enumItem) { value = enumItem.label; diff --git a/src/features/smartSearch/components/filters/PersonField/DisplayPersonField.tsx b/src/features/smartSearch/components/filters/PersonField/DisplayPersonField.tsx index 1f62cc80c..58276e5ff 100644 --- a/src/features/smartSearch/components/filters/PersonField/DisplayPersonField.tsx +++ b/src/features/smartSearch/components/filters/PersonField/DisplayPersonField.tsx @@ -38,7 +38,7 @@ const DisplayPersonField = ({ fieldType != 'date' && fieldType != 'text' && fieldType != 'url' && - fieldType != 'enum_text' + fieldType != 'enum' ) { // TODO: return null; @@ -55,7 +55,7 @@ const DisplayPersonField = ({ /> ); } else if ( - fieldType == 'enum_text' && + fieldType == 'enum' && field?.enum_choices && search !== undefined ) { diff --git a/src/features/smartSearch/components/filters/PersonField/index.tsx b/src/features/smartSearch/components/filters/PersonField/index.tsx index e6c7b978e..21816b89b 100644 --- a/src/features/smartSearch/components/filters/PersonField/index.tsx +++ b/src/features/smartSearch/components/filters/PersonField/index.tsx @@ -182,12 +182,12 @@ const PersonField = ({ /> ); } else if ( - type == CUSTOM_FIELD_TYPE.ENUM_TEXT && + type == CUSTOM_FIELD_TYPE.ENUM && selectedField?.enum_choices ) { return ( ( '{fieldSelect} is {timeFrame}' ), - enum_text: m<{ + enum: m<{ fieldSelect: ReactElement; selectInput: ReactElement; }>('{fieldSelect} is "{selectInput}"'), @@ -424,7 +424,7 @@ export default makeMessages('feat.smartSearch', { date: m<{ fieldName: ReactElement | string; timeFrame: ReactElement }>( '{fieldName} is {timeFrame}' ), - enum_text: m<{ + enum: m<{ fieldName: ReactElement | string; searchTerm: ReactElement | string; }>('{fieldName} is "{searchTerm}"'), diff --git a/src/utils/types/zetkin.ts b/src/utils/types/zetkin.ts index c02c17e4b..735cf81a8 100644 --- a/src/utils/types/zetkin.ts +++ b/src/utils/types/zetkin.ts @@ -454,7 +454,7 @@ export enum CUSTOM_FIELD_TYPE { DATE = 'date', TEXT = 'text', JSON = 'json', - ENUM_TEXT = 'enum_text', + ENUM = 'enum', } export interface ZetkinJourney { From e3e546967fc8881cbf95b254a7fe99ff1f312270 Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Thu, 17 Oct 2024 16:51:28 +0200 Subject: [PATCH 10/17] Submit null instead of '' for empty enum value --- src/features/import/hooks/useColumn.ts | 5 +---- .../components/EditPersonDialog/EditPersonFields.tsx | 8 ++++++-- .../profile/components/EditPersonDialog/index.tsx | 4 ++-- src/features/profile/hooks/useEditPerson.ts | 7 ++++--- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/features/import/hooks/useColumn.ts b/src/features/import/hooks/useColumn.ts index 1addc4493..c0f43bf32 100644 --- a/src/features/import/hooks/useColumn.ts +++ b/src/features/import/hooks/useColumn.ts @@ -75,10 +75,7 @@ export default function useColumn(orgId: number) { label: field.title, value: `date:${field.slug}`, }; - } else if ( - field.type == CUSTOM_FIELD_TYPE.ENUM && - field.enum_choices - ) { + } else if (field.type == CUSTOM_FIELD_TYPE.ENUM && field.enum_choices) { return { enumChoices: field.enum_choices, label: field.title, diff --git a/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx b/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx index ff330f3a3..d9cad813b 100644 --- a/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx +++ b/src/features/profile/components/EditPersonDialog/EditPersonFields.tsx @@ -35,7 +35,7 @@ enum GENDERS { interface EditPersonFieldsProps { fieldsToUpdate: ZetkinCreatePerson; invalidFields: string[]; - onChange: (field: string, newValue: string) => void; + onChange: (field: string, newValue: string | null) => void; onReset: (field: string) => void; orgId: number; fieldValues: ZetkinPerson; @@ -233,7 +233,11 @@ const EditPersonFields: FC = ({ fullWidth label={field.title} onChange={(ev) => { - onChange(field.slug, ev.target.value); + let value: string | null = ev.target.value; + if (value === '') { + value = null; + } + onChange(field.slug, value); }} value={fieldValues[field.slug]?.toString() ?? ''} > diff --git a/src/features/profile/components/EditPersonDialog/index.tsx b/src/features/profile/components/EditPersonDialog/index.tsx index 0995514fb..c5c28a76a 100644 --- a/src/features/profile/components/EditPersonDialog/index.tsx +++ b/src/features/profile/components/EditPersonDialog/index.tsx @@ -92,11 +92,11 @@ const EditPersonDialog: FC = ({ fieldValues={fieldValues} invalidFields={invalidFields} onChange={(field, newValue) => { - onFieldValueChange(field, newValue.trim()); + onFieldValueChange(field, newValue?.trim() ?? null); setFieldValues({ ...fieldValues, [field]: newValue }); }} onReset={(field) => { - onFieldValueChange(field, person[field]?.toString() ?? ''); + onFieldValueChange(field, person[field]?.toString() ?? null); setFieldValues({ ...fieldValues, [field]: person[field] }); }} orgId={orgId} diff --git a/src/features/profile/hooks/useEditPerson.ts b/src/features/profile/hooks/useEditPerson.ts index 0a77cd9cb..58f8ced07 100644 --- a/src/features/profile/hooks/useEditPerson.ts +++ b/src/features/profile/hooks/useEditPerson.ts @@ -22,11 +22,12 @@ export default function useEditPerson( const onFieldValueChange = ( field: keyof ZetkinUpdatePerson, - newValue: string + newValue: string | null ) => { - const isEmptyStringValue = !initialValues[field] && newValue === ''; + const isEmptyValue = + !initialValues[field] && (newValue === '' || newValue === null); - if (isEmptyStringValue || newValue === initialValues[field]?.toString()) { + if (isEmptyValue || newValue === initialValues[field]?.toString()) { const copied = { ...fieldsToUpdate }; delete copied[field]; setFieldsToUpdate(copied); From 31d681ae81e01bcd1addf26af2f80bce29157e0a Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Thu, 17 Oct 2024 21:08:18 +0200 Subject: [PATCH 11/17] Implement cellToString for PersonFeildColumnType --- .../ViewDataTable/columnTypes/PersonFieldColumnType.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/features/views/components/ViewDataTable/columnTypes/PersonFieldColumnType.tsx b/src/features/views/components/ViewDataTable/columnTypes/PersonFieldColumnType.tsx index 7b61753a3..f173ba289 100644 --- a/src/features/views/components/ViewDataTable/columnTypes/PersonFieldColumnType.tsx +++ b/src/features/views/components/ViewDataTable/columnTypes/PersonFieldColumnType.tsx @@ -9,8 +9,13 @@ type SimpleData = string | number | boolean | null; export default class PersonFieldColumnType implements IColumnType { - cellToString(): string { - return ''; + cellToString(cell: SimpleData): string { + if (this.enumChoices) { + const choice = this.enumChoices.find((c) => c.key == cell); + return choice ? choice.label : ''; + } else { + return cell != null ? cell.toString() : ''; + } } private enumChoices: EnumChoice[] | null = null; From 9faecf999bbd3737a29ba3f0f0204f915158567a Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Thu, 17 Oct 2024 23:41:02 +0200 Subject: [PATCH 12/17] Fix enum preview --- .../Configure/Preview/EnumPreview.tsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/features/import/components/ImportDialog/Configure/Preview/EnumPreview.tsx b/src/features/import/components/ImportDialog/Configure/Preview/EnumPreview.tsx index 1cabaa223..711ae7e91 100644 --- a/src/features/import/components/ImportDialog/Configure/Preview/EnumPreview.tsx +++ b/src/features/import/components/ImportDialog/Configure/Preview/EnumPreview.tsx @@ -17,12 +17,8 @@ const EnumPreview = ({ currentSheet, fieldKey, fields }: EnumPreviewProps) => { const messages = useMessages(messageIds); const { fieldOptions } = useColumn(orgId); - const hasMapped = currentSheet.columns.some( - (column) => - column.kind === ColumnKind.ENUM && - column.field === fieldKey && - column.mapping.length > 0 - ); + let hasMapped = false; + let enumKey: string | null = null; let columnHeader = ''; let enumOptions: EnumChoice[] | undefined = []; @@ -34,7 +30,22 @@ const EnumPreview = ({ currentSheet, fieldKey, fields }: EnumPreviewProps) => { }); const value = fields?.[fieldKey]; - const option = enumOptions.find((o) => o.key == value); + currentSheet.columns.forEach((column) => { + if ( + column.kind === ColumnKind.ENUM && + column.field === fieldKey && + column.mapping.length > 0 + ) { + hasMapped = true; + column.mapping.forEach((mappedColumn) => { + if (mappedColumn.value === value) { + enumKey = mappedColumn.key; + } + }); + } + }); + + const option = enumOptions.find((o) => o.key == enumKey); return ( Date: Thu, 17 Oct 2024 23:45:09 +0200 Subject: [PATCH 13/17] Fix message --- .../ImportDialog/Configure/Configuration/EnumConfigRow.tsx | 2 +- src/features/import/l10n/messageIds.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx index 4a616300c..fd2c4b953 100644 --- a/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx +++ b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx @@ -87,7 +87,7 @@ const EnumConfigRow: FC = ({ diff --git a/src/features/import/l10n/messageIds.ts b/src/features/import/l10n/messageIds.ts index d0c2bbd5e..a14ad1e9e 100644 --- a/src/features/import/l10n/messageIds.ts +++ b/src/features/import/l10n/messageIds.ts @@ -58,6 +58,9 @@ export default makeMessages('feat.import', { enum: { header: m('Map values to options'), none: m('None'), + numberOfRows: m<{ numRows: number }>( + '{numRows, plural, =1 {1 row} other {# rows}}' + ), value: m('Value'), }, ids: { From c6f88b5bedacd7bf76e969f5f133524f6da38f07 Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Thu, 17 Oct 2024 23:48:32 +0200 Subject: [PATCH 14/17] Use CUSTOM_FIELD_TYPE --- .../filters/PersonField/DisplayPersonField.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/features/smartSearch/components/filters/PersonField/DisplayPersonField.tsx b/src/features/smartSearch/components/filters/PersonField/DisplayPersonField.tsx index 58276e5ff..c4df49ae0 100644 --- a/src/features/smartSearch/components/filters/PersonField/DisplayPersonField.tsx +++ b/src/features/smartSearch/components/filters/PersonField/DisplayPersonField.tsx @@ -11,6 +11,7 @@ import UnderlinedMsg from '../../UnderlinedMsg'; import UnderlinedText from '../../UnderlinedText'; import useCustomFields from 'features/profile/hooks/useCustomFields'; import { useNumericRouteParams } from 'core/hooks'; +import { CUSTOM_FIELD_TYPE } from 'utils/types/zetkin'; const localMessageIds = messageIds.filters.personField; interface DisplayPersonFieldProps { @@ -35,16 +36,16 @@ const DisplayPersonField = ({ const fieldType = field?.type || ''; if ( - fieldType != 'date' && - fieldType != 'text' && - fieldType != 'url' && - fieldType != 'enum' + fieldType != CUSTOM_FIELD_TYPE.DATE && + fieldType != CUSTOM_FIELD_TYPE.TEXT && + fieldType != CUSTOM_FIELD_TYPE.URL && + fieldType != CUSTOM_FIELD_TYPE.ENUM ) { // TODO: return null; } let fieldMessage; - if (fieldType == 'date') { + if (fieldType == CUSTOM_FIELD_TYPE.DATE) { fieldMessage = ( ); } else if ( - fieldType == 'enum' && + fieldType == CUSTOM_FIELD_TYPE.ENUM && field?.enum_choices && search !== undefined ) { From 56adf2dbaa40a1616a837021cf73444d2b3fe03d Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Fri, 18 Oct 2024 12:55:39 +0200 Subject: [PATCH 15/17] Separate DATE and ENUM conditionals in useColumn --- src/features/import/hooks/useColumn.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/features/import/hooks/useColumn.ts b/src/features/import/hooks/useColumn.ts index c0f43bf32..ffdc6eab8 100644 --- a/src/features/import/hooks/useColumn.ts +++ b/src/features/import/hooks/useColumn.ts @@ -54,7 +54,11 @@ export default function useColumn(orgId: number) { return column.field == value.slice(6); } - if (column.kind == ColumnKind.DATE || column.kind == ColumnKind.ENUM) { + if (column.kind == ColumnKind.DATE) { + return column.field == value.slice(5); + } + + if (column.kind == ColumnKind.ENUM) { return column.field == value.slice(5); } }); From 660e6570240ebbef048b3a881ccaf26ada444e56 Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Fri, 18 Oct 2024 13:04:18 +0200 Subject: [PATCH 16/17] Add mapping button in EnumConfigRow --- .../Configure/Configuration/EnumConfigRow.tsx | 73 ++++++++++++------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx index fd2c4b953..cf4da5baf 100644 --- a/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx +++ b/src/features/import/components/ImportDialog/Configure/Configuration/EnumConfigRow.tsx @@ -1,6 +1,7 @@ import { ArrowForward, Delete } from '@mui/icons-material'; import { Box, + Button, FormControl, IconButton, InputLabel, @@ -8,7 +9,7 @@ import { Select, Typography, } from '@mui/material'; -import { FC } from 'react'; +import { FC, useState } from 'react'; import messageIds from 'features/import/l10n/messageIds'; import { EnumChoice } from 'utils/types/zetkin'; @@ -34,6 +35,9 @@ const EnumConfigRow: FC = ({ title, }) => { const messages = useMessages(messageIds); + const [mapping, setMapping] = useState(false); + + const showSelect = mapping || selectedOption; return ( @@ -56,33 +60,48 @@ const EnumConfigRow: FC = ({ paddingRight={1} width="50%" > - - - - - - - { - onDeselectOption(); - }} - > - - + /> + + )} + {showSelect && ( + <> + + + + + + + { + onDeselectOption(); + setMapping(false); + }} + > + + + + )} From d641dd6319e39bb9351ca7d94631ccc1d93f1371 Mon Sep 17 00:00:00 2001 From: Niklas Vanhainen Date: Fri, 18 Oct 2024 13:51:44 +0200 Subject: [PATCH 17/17] Remove enumChoices from Option and instead load the custom fields in EnumPreview. --- .../ImportDialog/Configure/Preview/EnumPreview.tsx | 10 ++++++---- src/features/import/hooks/useColumn.ts | 4 +--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/features/import/components/ImportDialog/Configure/Preview/EnumPreview.tsx b/src/features/import/components/ImportDialog/Configure/Preview/EnumPreview.tsx index 711ae7e91..e5bec5d87 100644 --- a/src/features/import/components/ImportDialog/Configure/Preview/EnumPreview.tsx +++ b/src/features/import/components/ImportDialog/Configure/Preview/EnumPreview.tsx @@ -1,10 +1,10 @@ import messageIds from 'features/import/l10n/messageIds'; import PreviewGrid from './PreviewGrid'; import { useMessages } from 'core/i18n'; -import { EnumChoice } from 'utils/types/zetkin'; import { CellData, ColumnKind, Sheet } from 'features/import/utils/types'; import useColumn from 'features/import/hooks/useColumn'; import { useNumericRouteParams } from 'core/hooks'; +import useCustomFields from 'features/profile/hooks/useCustomFields'; interface EnumPreviewProps { currentSheet: Sheet; @@ -16,19 +16,21 @@ const EnumPreview = ({ currentSheet, fieldKey, fields }: EnumPreviewProps) => { const { orgId } = useNumericRouteParams(); const messages = useMessages(messageIds); const { fieldOptions } = useColumn(orgId); + const customFields = useCustomFields(orgId).data ?? []; let hasMapped = false; let enumKey: string | null = null; let columnHeader = ''; - let enumOptions: EnumChoice[] | undefined = []; fieldOptions.flat().forEach((columnOp) => { if (columnOp.value === `enum:${fieldKey}`) { columnHeader = columnOp.label; - enumOptions = columnOp.enumChoices; } }); + const field = customFields.find((f) => f.slug === fieldKey); + const enumChoices = field?.enum_choices || []; + const value = fields?.[fieldKey]; currentSheet.columns.forEach((column) => { if ( @@ -45,7 +47,7 @@ const EnumPreview = ({ currentSheet, fieldKey, fields }: EnumPreviewProps) => { } }); - const option = enumOptions.find((o) => o.key == enumKey); + const option = enumChoices.find((o) => o.key == enumKey); return (