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:
@ -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 [];
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -5,4 +5,5 @@ export enum SpreadsheetColumnType {
|
||||
matchedCheckbox,
|
||||
matchedSelect,
|
||||
matchedSelectOptions,
|
||||
matchedError,
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user