Import - fixes (#12569)

<img width="800" alt="Screenshot 2025-06-12 at 15 22 49"
src="https://github.com/user-attachments/assets/afaa4ef1-b16c-4c05-ba4a-d77ad2ccfa76"
/>


To test : 
- unselect an option on select/multi-select matching (matching step)
- match a mutli-select field with an other field

closes : https://github.com/twentyhq/core-team-issues/issues/1065
closes : https://github.com/twentyhq/core-team-issues/issues/1066
This commit is contained in:
Etienne
2025-06-13 15:52:32 +02:00
committed by GitHub
parent 6bca562c65
commit 8e007c8b5f
9 changed files with 89 additions and 35 deletions

View File

@ -1,3 +1,4 @@
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
import { emailSchema } from '@/object-record/record-field/validation-schemas/emailSchema';
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types';
import { t } from '@lingui/core/macro';
@ -214,6 +215,22 @@ export const getSpreadSheetFieldValidationDefinitions = (
level: 'error',
},
];
case FieldMetadataType.RATING: {
const ratingValues = RATING_VALUES.join(', ');
return [
{
rule: 'function',
isValid: (value: string) => {
return RATING_VALUES.includes(
value as (typeof RATING_VALUES)[number],
);
},
errorMessage: `${fieldName} ${t` must be one of ${ratingValues} values`}`,
level: 'error',
},
];
}
default:
return [];
}

View File

@ -1,13 +1,7 @@
import { SubMatchingSelectControlContainer } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectControlContainer';
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconChevronDown } from 'twenty-ui/display';
const StyledIconChevronDown = styled(IconChevronDown)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
const StyledLabel = styled.span`
color: ${({ theme }) => theme.font.color.primary};
@ -28,17 +22,11 @@ export type SubMatchingSelectRowLeftSelectProps<T> = {
export const SubMatchingSelectRowLeftSelect = <T extends string>({
option,
}: SubMatchingSelectRowLeftSelectProps<T>) => {
const theme = useTheme();
return (
<SubMatchingSelectControlContainer cursor="default">
<StyledControlLabel>
<StyledLabel>{option.entry}</StyledLabel>
</StyledControlLabel>
<StyledIconChevronDown
size={theme.font.size.md}
color={theme.font.color.tertiary}
/>
</SubMatchingSelectControlContainer>
);
};

View File

@ -18,6 +18,13 @@ const StyledContainer = styled.div`
width: 100%;
`;
const StyledErrorMessage = styled.span`
color: ${({ theme }) => theme.font.color.danger};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin-top: ${({ theme }) => theme.spacing(1)};
`;
type TemplateColumnProps<T extends string> = {
columns: SpreadsheetColumns<string>;
columnIndex: number;
@ -72,6 +79,9 @@ export const TemplateColumn = <T extends string>({
suggestedOptions={suggestedFieldOptions}
columnIndex={column.index.toString()}
/>
{column.type === SpreadsheetColumnType.matchedError && (
<StyledErrorMessage>{`"${column.header}" ${column.errorMessage}`}</StyledErrorMessage>
)}
</StyledContainer>
);
};

View File

@ -4,6 +4,7 @@ import { UnmatchColumnBanner } from '@/spreadsheet-import/steps/components/Match
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
@ -54,6 +55,8 @@ export const UnmatchColumn = <T extends string>({
const isSelect = 'matchedOptions' in column;
const { t } = useLingui();
const allMatched = column.type === SpreadsheetColumnType.matchedSelectOptions;
if (!isSelect) return null;
return (
@ -62,6 +65,7 @@ export const UnmatchColumn = <T extends string>({
message={getExpandableContainerTitle(fields, column)}
buttonOnClick={() => setIsExpanded(!isExpanded)}
isExpanded={isExpanded}
allMatched={allMatched}
/>
<AnimatedExpandableContainer
isExpanded={isExpanded}

View File

@ -3,14 +3,16 @@ import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
import { Banner, IconChevronDown, IconInfoCircle } from 'twenty-ui/display';
const StyledBanner = styled(Banner)`
background: ${({ theme }) => theme.accent.secondary};
const StyledBanner = styled(Banner)<{ allMatched: boolean }>`
background: ${({ allMatched, theme }) =>
allMatched ? theme.accent.secondary : theme.background.transparent.light};
border-radius: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2) + ' ' + theme.spacing(2.5)};
`;
const StyledText = styled.div`
color: ${({ theme }) => theme.color.blue};
const StyledText = styled.div<{ allMatched: boolean }>`
color: ${({ allMatched, theme }) =>
allMatched ? theme.color.blue : theme.font.color.secondary};
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
@ -19,8 +21,10 @@ const StyledText = styled.div`
const StyledTransitionedIconChevronDown = styled(IconChevronDown)<{
isExpanded: boolean;
allMatched: boolean;
}>`
color: ${({ theme }) => theme.color.blue};
color: ${({ allMatched, theme }) =>
allMatched ? theme.color.blue : theme.font.color.secondary};
transform: ${({ isExpanded }) =>
isExpanded ? 'rotate(180deg)' : 'rotate(0deg)'};
transition: ${({ theme }) =>
@ -42,26 +46,32 @@ export const UnmatchColumnBanner = ({
message,
isExpanded,
buttonOnClick,
allMatched,
}: {
message: string;
isExpanded: boolean;
buttonOnClick?: () => void;
allMatched: boolean;
}) => {
const theme = useTheme();
return (
<StyledBanner>
<IconInfoCircle color={theme.color.blue} size={theme.icon.size.md} />
<StyledBanner allMatched={allMatched}>
<IconInfoCircle
color={allMatched ? theme.color.blue : theme.font.color.secondary}
size={theme.icon.size.md}
/>
{isDefined(buttonOnClick) ? (
<StyledClickableContainer onClick={buttonOnClick}>
<StyledText>{message}</StyledText>
<StyledText allMatched={allMatched}>{message}</StyledText>
<StyledTransitionedIconChevronDown
isExpanded={isExpanded}
allMatched={allMatched}
size={theme.icon.size.md}
/>
</StyledClickableContainer>
) : (
<StyledText>{message}</StyledText>
<StyledText allMatched={allMatched}>{message}</StyledText>
)}
</StyledBanner>
);

View File

@ -43,10 +43,19 @@ export type SpreadsheetMatchedSelectOptionsColumn<T> = {
matchedOptions: SpreadsheetMatchedOptions<T>[];
};
export type SpreadsheetErrorColumn<T> = {
type: SpreadsheetColumnType.matchedError;
index: number;
header: string;
value: T;
errorMessage: string;
};
export type SpreadsheetColumn<T extends string> =
| SpreadsheetEmptyColumn
| SpreadsheetIgnoredColumn
| SpreadsheetMatchedColumn<T>
| SpreadsheetMatchedSwitchColumn<T>
| SpreadsheetMatchedSelectColumn<T>
| SpreadsheetMatchedSelectOptionsColumn<T>;
| SpreadsheetMatchedSelectOptionsColumn<T>
| SpreadsheetErrorColumn<T>;

View File

@ -5,4 +5,5 @@ export enum SpreadsheetColumnType {
matchedCheckbox,
matchedSelect,
matchedSelectOptions,
matchedError,
}

View File

@ -4,6 +4,7 @@ import { SpreadsheetImportField } from '@/spreadsheet-import/types';
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
import { t } from '@lingui/core/macro';
import { z } from 'zod';
import { uniqueEntries } from './uniqueEntries';
@ -45,21 +46,28 @@ export const setColumn = <T extends string>(
if (field?.fieldType.type === 'multiSelect') {
const fieldOptions = field.fieldType.options;
const entries = [
...new Set(
data
?.flatMap((row) => {
try {
let entries: string[] = [];
try {
entries = [
...new Set(
data
?.flatMap((row) => {
const value = row[oldColumn.index];
const options = JSON.parse(z.string().parse(value));
return z.array(z.string()).parse(options);
} catch {
return [];
}
})
.filter((entry) => typeof entry === 'string'),
),
];
})
.filter((entry) => typeof entry === 'string'),
),
];
} catch {
return {
index: oldColumn.index,
header: oldColumn.header,
type: SpreadsheetColumnType.matchedError,
value: field.key,
errorMessage: t`column data is not compatible with Multi-Select.`,
};
}
const matchedOptions = entries.map((entry) => {
const value = fieldOptions.find(

View File

@ -14,9 +14,16 @@ export const setSubColumn = <T>(
):
| SpreadsheetMatchedSelectColumn<T>
| SpreadsheetMatchedSelectOptionsColumn<T> => {
const shouldUnselectValue =
oldColumn.matchedOptions.find((option) => option.entry === entry)?.value ===
value;
const options = oldColumn.matchedOptions.map((option) =>
option.entry === entry ? { ...option, value } : option,
option.entry === entry
? { ...option, value: shouldUnselectValue ? undefined : value }
: option,
);
const allMatched = options.every(({ value }) => !!value);
if (allMatched) {
return {