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 { emailSchema } from '@/object-record/record-field/validation-schemas/emailSchema';
|
||||||
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types';
|
import { SpreadsheetImportFieldValidationDefinition } from '@/spreadsheet-import/types';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
@ -214,6 +215,22 @@ export const getSpreadSheetFieldValidationDefinitions = (
|
|||||||
level: 'error',
|
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:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,7 @@
|
|||||||
import { SubMatchingSelectControlContainer } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectControlContainer';
|
import { SubMatchingSelectControlContainer } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectControlContainer';
|
||||||
|
|
||||||
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
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`
|
const StyledLabel = styled.span`
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
@ -28,17 +22,11 @@ export type SubMatchingSelectRowLeftSelectProps<T> = {
|
|||||||
export const SubMatchingSelectRowLeftSelect = <T extends string>({
|
export const SubMatchingSelectRowLeftSelect = <T extends string>({
|
||||||
option,
|
option,
|
||||||
}: SubMatchingSelectRowLeftSelectProps<T>) => {
|
}: SubMatchingSelectRowLeftSelectProps<T>) => {
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMatchingSelectControlContainer cursor="default">
|
<SubMatchingSelectControlContainer cursor="default">
|
||||||
<StyledControlLabel>
|
<StyledControlLabel>
|
||||||
<StyledLabel>{option.entry}</StyledLabel>
|
<StyledLabel>{option.entry}</StyledLabel>
|
||||||
</StyledControlLabel>
|
</StyledControlLabel>
|
||||||
<StyledIconChevronDown
|
|
||||||
size={theme.font.size.md}
|
|
||||||
color={theme.font.color.tertiary}
|
|
||||||
/>
|
|
||||||
</SubMatchingSelectControlContainer>
|
</SubMatchingSelectControlContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -18,6 +18,13 @@ const StyledContainer = styled.div`
|
|||||||
width: 100%;
|
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> = {
|
type TemplateColumnProps<T extends string> = {
|
||||||
columns: SpreadsheetColumns<string>;
|
columns: SpreadsheetColumns<string>;
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
@ -72,6 +79,9 @@ export const TemplateColumn = <T extends string>({
|
|||||||
suggestedOptions={suggestedFieldOptions}
|
suggestedOptions={suggestedFieldOptions}
|
||||||
columnIndex={column.index.toString()}
|
columnIndex={column.index.toString()}
|
||||||
/>
|
/>
|
||||||
|
{column.type === SpreadsheetColumnType.matchedError && (
|
||||||
|
<StyledErrorMessage>{`"${column.header}" ${column.errorMessage}`}</StyledErrorMessage>
|
||||||
|
)}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { UnmatchColumnBanner } from '@/spreadsheet-import/steps/components/Match
|
|||||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||||
|
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -54,6 +55,8 @@ export const UnmatchColumn = <T extends string>({
|
|||||||
const isSelect = 'matchedOptions' in column;
|
const isSelect = 'matchedOptions' in column;
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const allMatched = column.type === SpreadsheetColumnType.matchedSelectOptions;
|
||||||
|
|
||||||
if (!isSelect) return null;
|
if (!isSelect) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -62,6 +65,7 @@ export const UnmatchColumn = <T extends string>({
|
|||||||
message={getExpandableContainerTitle(fields, column)}
|
message={getExpandableContainerTitle(fields, column)}
|
||||||
buttonOnClick={() => setIsExpanded(!isExpanded)}
|
buttonOnClick={() => setIsExpanded(!isExpanded)}
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
|
allMatched={allMatched}
|
||||||
/>
|
/>
|
||||||
<AnimatedExpandableContainer
|
<AnimatedExpandableContainer
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
|
|||||||
@ -3,14 +3,16 @@ import styled from '@emotion/styled';
|
|||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { Banner, IconChevronDown, IconInfoCircle } from 'twenty-ui/display';
|
import { Banner, IconChevronDown, IconInfoCircle } from 'twenty-ui/display';
|
||||||
|
|
||||||
const StyledBanner = styled(Banner)`
|
const StyledBanner = styled(Banner)<{ allMatched: boolean }>`
|
||||||
background: ${({ theme }) => theme.accent.secondary};
|
background: ${({ allMatched, theme }) =>
|
||||||
|
allMatched ? theme.accent.secondary : theme.background.transparent.light};
|
||||||
border-radius: ${({ theme }) => theme.spacing(2)};
|
border-radius: ${({ theme }) => theme.spacing(2)};
|
||||||
padding: ${({ theme }) => theme.spacing(2) + ' ' + theme.spacing(2.5)};
|
padding: ${({ theme }) => theme.spacing(2) + ' ' + theme.spacing(2.5)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledText = styled.div`
|
const StyledText = styled.div<{ allMatched: boolean }>`
|
||||||
color: ${({ theme }) => theme.color.blue};
|
color: ${({ allMatched, theme }) =>
|
||||||
|
allMatched ? theme.color.blue : theme.font.color.secondary};
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -19,8 +21,10 @@ const StyledText = styled.div`
|
|||||||
|
|
||||||
const StyledTransitionedIconChevronDown = styled(IconChevronDown)<{
|
const StyledTransitionedIconChevronDown = styled(IconChevronDown)<{
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
|
allMatched: boolean;
|
||||||
}>`
|
}>`
|
||||||
color: ${({ theme }) => theme.color.blue};
|
color: ${({ allMatched, theme }) =>
|
||||||
|
allMatched ? theme.color.blue : theme.font.color.secondary};
|
||||||
transform: ${({ isExpanded }) =>
|
transform: ${({ isExpanded }) =>
|
||||||
isExpanded ? 'rotate(180deg)' : 'rotate(0deg)'};
|
isExpanded ? 'rotate(180deg)' : 'rotate(0deg)'};
|
||||||
transition: ${({ theme }) =>
|
transition: ${({ theme }) =>
|
||||||
@ -42,26 +46,32 @@ export const UnmatchColumnBanner = ({
|
|||||||
message,
|
message,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
buttonOnClick,
|
buttonOnClick,
|
||||||
|
allMatched,
|
||||||
}: {
|
}: {
|
||||||
message: string;
|
message: string;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
buttonOnClick?: () => void;
|
buttonOnClick?: () => void;
|
||||||
|
allMatched: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledBanner>
|
<StyledBanner allMatched={allMatched}>
|
||||||
<IconInfoCircle color={theme.color.blue} size={theme.icon.size.md} />
|
<IconInfoCircle
|
||||||
|
color={allMatched ? theme.color.blue : theme.font.color.secondary}
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
/>
|
||||||
{isDefined(buttonOnClick) ? (
|
{isDefined(buttonOnClick) ? (
|
||||||
<StyledClickableContainer onClick={buttonOnClick}>
|
<StyledClickableContainer onClick={buttonOnClick}>
|
||||||
<StyledText>{message}</StyledText>
|
<StyledText allMatched={allMatched}>{message}</StyledText>
|
||||||
<StyledTransitionedIconChevronDown
|
<StyledTransitionedIconChevronDown
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
|
allMatched={allMatched}
|
||||||
size={theme.icon.size.md}
|
size={theme.icon.size.md}
|
||||||
/>
|
/>
|
||||||
</StyledClickableContainer>
|
</StyledClickableContainer>
|
||||||
) : (
|
) : (
|
||||||
<StyledText>{message}</StyledText>
|
<StyledText allMatched={allMatched}>{message}</StyledText>
|
||||||
)}
|
)}
|
||||||
</StyledBanner>
|
</StyledBanner>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -43,10 +43,19 @@ export type SpreadsheetMatchedSelectOptionsColumn<T> = {
|
|||||||
matchedOptions: SpreadsheetMatchedOptions<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> =
|
export type SpreadsheetColumn<T extends string> =
|
||||||
| SpreadsheetEmptyColumn
|
| SpreadsheetEmptyColumn
|
||||||
| SpreadsheetIgnoredColumn
|
| SpreadsheetIgnoredColumn
|
||||||
| SpreadsheetMatchedColumn<T>
|
| SpreadsheetMatchedColumn<T>
|
||||||
| SpreadsheetMatchedSwitchColumn<T>
|
| SpreadsheetMatchedSwitchColumn<T>
|
||||||
| SpreadsheetMatchedSelectColumn<T>
|
| SpreadsheetMatchedSelectColumn<T>
|
||||||
| SpreadsheetMatchedSelectOptionsColumn<T>;
|
| SpreadsheetMatchedSelectOptionsColumn<T>
|
||||||
|
| SpreadsheetErrorColumn<T>;
|
||||||
|
|||||||
@ -5,4 +5,5 @@ export enum SpreadsheetColumnType {
|
|||||||
matchedCheckbox,
|
matchedCheckbox,
|
||||||
matchedSelect,
|
matchedSelect,
|
||||||
matchedSelectOptions,
|
matchedSelectOptions,
|
||||||
|
matchedError,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { SpreadsheetImportField } from '@/spreadsheet-import/types';
|
|||||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||||
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { uniqueEntries } from './uniqueEntries';
|
import { uniqueEntries } from './uniqueEntries';
|
||||||
|
|
||||||
@ -45,21 +46,28 @@ export const setColumn = <T extends string>(
|
|||||||
if (field?.fieldType.type === 'multiSelect') {
|
if (field?.fieldType.type === 'multiSelect') {
|
||||||
const fieldOptions = field.fieldType.options;
|
const fieldOptions = field.fieldType.options;
|
||||||
|
|
||||||
const entries = [
|
let entries: string[] = [];
|
||||||
...new Set(
|
try {
|
||||||
data
|
entries = [
|
||||||
?.flatMap((row) => {
|
...new Set(
|
||||||
try {
|
data
|
||||||
|
?.flatMap((row) => {
|
||||||
const value = row[oldColumn.index];
|
const value = row[oldColumn.index];
|
||||||
const options = JSON.parse(z.string().parse(value));
|
const options = JSON.parse(z.string().parse(value));
|
||||||
return z.array(z.string()).parse(options);
|
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 matchedOptions = entries.map((entry) => {
|
||||||
const value = fieldOptions.find(
|
const value = fieldOptions.find(
|
||||||
|
|||||||
@ -14,9 +14,16 @@ export const setSubColumn = <T>(
|
|||||||
):
|
):
|
||||||
| SpreadsheetMatchedSelectColumn<T>
|
| SpreadsheetMatchedSelectColumn<T>
|
||||||
| SpreadsheetMatchedSelectOptionsColumn<T> => {
|
| SpreadsheetMatchedSelectOptionsColumn<T> => {
|
||||||
|
const shouldUnselectValue =
|
||||||
|
oldColumn.matchedOptions.find((option) => option.entry === entry)?.value ===
|
||||||
|
value;
|
||||||
|
|
||||||
const options = oldColumn.matchedOptions.map((option) =>
|
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);
|
const allMatched = options.every(({ value }) => !!value);
|
||||||
if (allMatched) {
|
if (allMatched) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user