Fix CSV import select field matching (#11361)
This PR fixes a bug that prevented to do the matching of an imported CSV file that contains a SELECT type column. Fixes https://github.com/twentyhq/twenty/issues/11220 ## Stacking context improvement During the development it was clear that we lacked a reliable way to understand our own z indices for components like modal, portaled dropdown, overlay background, etc. So in this PR we introduce a new enum RootStackingContextZIndices, this enum allows to keep track of our root stacking context component z-index, and because it is an enum, it prevents any conflict. See https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context for reference. ## Component cleaning Components have been reorganized in a SubMatchingSelectRow component The Dropdown component has been used to replace the SelectInput component which doesn't fit this use case because we are not in a cell, we just need a simple standalone dropdown, though it would be interesting to extract the UI part of the SelectInput, to share it here, the benefit is not obvious since we already have good shared components like Tag and Dropdown to implement this specific use case. --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -23,6 +23,7 @@ import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/reco
|
||||
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
|
||||
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -46,7 +47,7 @@ const StyledCommandMenu = styled(motion.div)`
|
||||
position: fixed;
|
||||
right: 0%;
|
||||
top: 0%;
|
||||
z-index: 30;
|
||||
z-index: ${RootStackingContextZIndices.CommandMenu};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
@ -1,39 +1,26 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
autoUpdate,
|
||||
flip,
|
||||
offset,
|
||||
size,
|
||||
useFloating,
|
||||
} from '@floating-ui/react';
|
||||
import React, { useCallback, useId, useRef, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ReadonlyDeep } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
||||
import { AppTooltip } from 'twenty-ui/display';
|
||||
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { SelectOption } from 'twenty-ui/input';
|
||||
|
||||
const StyledFloatingDropdown = styled.div`
|
||||
z-index: ${({ theme }) => theme.lastLayerZIndex};
|
||||
`;
|
||||
import { MenuItem, MenuItemSelect } from 'twenty-ui/navigation';
|
||||
import { v4 } from 'uuid';
|
||||
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
||||
|
||||
interface MatchColumnSelectProps {
|
||||
columnIndex: string;
|
||||
onChange: (value: ReadonlyDeep<SelectOption> | null) => void;
|
||||
value?: ReadonlyDeep<SelectOption>;
|
||||
options: readonly ReadonlyDeep<SelectOption>[];
|
||||
placeholder?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export const MatchColumnSelect = ({
|
||||
@ -41,30 +28,15 @@ export const MatchColumnSelect = ({
|
||||
value,
|
||||
options: initialOptions,
|
||||
placeholder,
|
||||
columnIndex,
|
||||
}: MatchColumnSelectProps) => {
|
||||
const theme = useTheme();
|
||||
const idPrefix = useId();
|
||||
const dropdownId = `match-column-select-dropdown-${columnIndex}`;
|
||||
|
||||
const dropdownContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { closeDropdown } = useDropdown(dropdownId);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const [options, setOptions] = useState(initialOptions);
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
strategy: 'absolute',
|
||||
middleware: [
|
||||
offset(() => {
|
||||
return parseInt(theme.spacing(2), 10);
|
||||
}),
|
||||
flip(),
|
||||
size(),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
open: isOpen,
|
||||
placement: 'bottom-start',
|
||||
});
|
||||
|
||||
const handleSearchFilterChange = useCallback(
|
||||
(text: string) => {
|
||||
setOptions(
|
||||
@ -91,23 +63,11 @@ export const MatchColumnSelect = ({
|
||||
debouncedHandleSearchFilter(value);
|
||||
};
|
||||
|
||||
const handleDropdownItemClick = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const handleChange = (option: ReadonlyDeep<SelectOption>) => {
|
||||
onChange(option);
|
||||
setIsOpen(false);
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [dropdownContainerRef],
|
||||
callback: () => {
|
||||
setIsOpen(false);
|
||||
},
|
||||
listenerId: 'match-column-select',
|
||||
});
|
||||
|
||||
useUpdateEffect(() => {
|
||||
setOptions(initialOptions);
|
||||
}, [initialOptions]);
|
||||
@ -115,70 +75,64 @@ export const MatchColumnSelect = ({
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={refs.setReference}>
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
}}
|
||||
dropdownPlacement="bottom-start"
|
||||
clickableComponent={
|
||||
<MenuItem
|
||||
LeftIcon={value?.Icon}
|
||||
onClick={handleDropdownItemClick}
|
||||
text={value?.label ?? placeholder ?? ''}
|
||||
accent={value?.label ? 'default' : 'placeholder'}
|
||||
/>
|
||||
</div>
|
||||
{isOpen &&
|
||||
createPortal(
|
||||
<StyledFloatingDropdown ref={refs.setFloating} style={floatingStyles}>
|
||||
<OverlayContainer>
|
||||
<DropdownMenu
|
||||
data-select-disable
|
||||
ref={dropdownContainerRef}
|
||||
// width={refs.domReference.current?.clientWidth}
|
||||
>
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{options?.map((option, index) => {
|
||||
const id = `${idPrefix}-option-${index}`;
|
||||
return (
|
||||
<React.Fragment key={id}>
|
||||
<div id={id}>
|
||||
<MenuItemSelect
|
||||
selected={value?.label === option.label}
|
||||
onClick={() => handleChange(option)}
|
||||
disabled={
|
||||
option.disabled && value?.value !== option.value
|
||||
}
|
||||
LeftIcon={option?.Icon}
|
||||
text={option.label}
|
||||
/>
|
||||
</div>
|
||||
{option.disabled &&
|
||||
value?.value !== option.value &&
|
||||
createPortal(
|
||||
<AppTooltip
|
||||
key={id}
|
||||
anchorSelect={`#${id}`}
|
||||
content={t`You are already importing this column.`}
|
||||
place="right"
|
||||
offset={-20}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{options?.length === 0 && (
|
||||
<MenuItem key="No results" text={t`No results`} />
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
</OverlayContainer>
|
||||
</StyledFloatingDropdown>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
}
|
||||
dropdownComponents={
|
||||
<>
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{options?.map((option) => {
|
||||
const id = `${v4()}-${option.value}`;
|
||||
return (
|
||||
<React.Fragment key={id}>
|
||||
<div id={id}>
|
||||
<MenuItemSelect
|
||||
selected={value?.label === option.label}
|
||||
onClick={() => handleChange(option)}
|
||||
disabled={
|
||||
option.disabled && value?.value !== option.value
|
||||
}
|
||||
LeftIcon={option?.Icon}
|
||||
text={option.label}
|
||||
/>
|
||||
</div>
|
||||
{option.disabled &&
|
||||
value?.value !== option.value &&
|
||||
createPortal(
|
||||
<AppTooltip
|
||||
key={id}
|
||||
anchorSelect={`#${id}`}
|
||||
content={t`You are already importing this column.`}
|
||||
place="right"
|
||||
offset={-20}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{options?.length === 0 && (
|
||||
<MenuItem key="No results" text={t`No results`} />
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -14,9 +14,9 @@ import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/Spreadshee
|
||||
import { SelectInput } from '@/ui/input/components/SelectInput';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Tag, TagColor } from 'twenty-ui/components';
|
||||
import { IconChevronDown } from 'twenty-ui/display';
|
||||
import { SelectOption } from 'twenty-ui/input';
|
||||
import { Tag, TagColor } from 'twenty-ui/components';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledControlContainer = styled.div<{ cursor: string }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
box-sizing: border-box;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
cursor: ${({ cursor }) => cursor};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
justify-content: space-between;
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SubMatchingSelectControlContainer = StyledControlContainer;
|
||||
@ -0,0 +1,54 @@
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { SubMatchingSelectControlContainer } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectControlContainer';
|
||||
import {
|
||||
SpreadsheetMatchedSelectColumn,
|
||||
SpreadsheetMatchedSelectOptionsColumn,
|
||||
} from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
|
||||
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
||||
import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Tag, TagColor } from 'twenty-ui/components';
|
||||
import { IconChevronDown } from 'twenty-ui/display';
|
||||
import { SelectOption } from 'twenty-ui/input';
|
||||
const StyledIconChevronDown = styled(IconChevronDown)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
export type SubMatchingSelectDropdownButtonProps<T> = {
|
||||
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
||||
column:
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T>;
|
||||
placeholder: string;
|
||||
};
|
||||
|
||||
export const SubMatchingSelectDropdownButton = <T extends string>({
|
||||
option,
|
||||
column,
|
||||
placeholder,
|
||||
}: SubMatchingSelectDropdownButtonProps<T>) => {
|
||||
const { openDropdown } = useDropdown();
|
||||
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
||||
const value = options.find((opt) => opt.value === option.value);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<SubMatchingSelectControlContainer
|
||||
cursor="pointer"
|
||||
onClick={() => openDropdown()}
|
||||
id="control"
|
||||
>
|
||||
<Tag
|
||||
text={value?.label ?? placeholder}
|
||||
color={value?.color as TagColor}
|
||||
/>
|
||||
<StyledIconChevronDown size={theme.icon.size.md} />
|
||||
</SubMatchingSelectControlContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,72 @@
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { TagColor } from 'twenty-ui/components';
|
||||
import { SelectOption } from 'twenty-ui/input';
|
||||
import { MenuItemSelectTag } from 'twenty-ui/navigation';
|
||||
|
||||
interface SubMatchingSelectInputProps {
|
||||
onOptionSelected: (selectedOption: SelectOption) => void;
|
||||
options: SelectOption[];
|
||||
defaultOption?: SelectOption;
|
||||
}
|
||||
|
||||
export const SubMatchingSelectInput = ({
|
||||
onOptionSelected,
|
||||
options,
|
||||
defaultOption,
|
||||
}: SubMatchingSelectInputProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
const [selectedOption, setSelectedOption] = useState<
|
||||
SelectOption | undefined
|
||||
>(defaultOption);
|
||||
|
||||
const optionsToSelect = useMemo(
|
||||
() =>
|
||||
options.filter((option) => {
|
||||
return (
|
||||
option.value !== selectedOption?.value &&
|
||||
option.label.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
);
|
||||
}) || [],
|
||||
[options, searchFilter, selectedOption?.value],
|
||||
);
|
||||
|
||||
const optionsInDropDown = useMemo(
|
||||
() =>
|
||||
selectedOption ? [selectedOption, ...optionsToSelect] : optionsToSelect,
|
||||
[optionsToSelect, selectedOption],
|
||||
);
|
||||
|
||||
const handleOptionChange = (option: SelectOption) => {
|
||||
setSelectedOption(option);
|
||||
onOptionSelected(option);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu ref={containerRef} data-select-disable>
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{optionsInDropDown.map((option) => (
|
||||
<MenuItemSelectTag
|
||||
key={option.value}
|
||||
selected={selectedOption?.value === option.value}
|
||||
text={option.label}
|
||||
color={(option.color as TagColor) ?? 'transparent'}
|
||||
onClick={() => handleOptionChange(option)}
|
||||
LeftIcon={option.Icon}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,46 @@
|
||||
import { SubMatchingSelectRowLeftSelect } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRowLeftSelect';
|
||||
import { SubMatchingSelectRowRightDropdown } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRowRightDropdown';
|
||||
import {
|
||||
SpreadsheetMatchedSelectColumn,
|
||||
SpreadsheetMatchedSelectOptionsColumn,
|
||||
} from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledRowContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
justify-content: space-between;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
interface SubMatchingSelectRowProps<T> {
|
||||
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
||||
column:
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T>;
|
||||
onSubChange: (val: T, index: number, option: string) => void;
|
||||
placeholder: string;
|
||||
selectedOption?:
|
||||
| SpreadsheetMatchedOptions<T>
|
||||
| Partial<SpreadsheetMatchedOptions<T>>;
|
||||
}
|
||||
export const SubMatchingSelectRow = <T extends string>({
|
||||
option,
|
||||
column,
|
||||
onSubChange,
|
||||
placeholder,
|
||||
}: SubMatchingSelectRowProps<T>) => {
|
||||
return (
|
||||
<StyledRowContainer>
|
||||
<SubMatchingSelectRowLeftSelect option={option} />
|
||||
<SubMatchingSelectRowRightDropdown
|
||||
column={column}
|
||||
onSubChange={onSubChange}
|
||||
option={option}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</StyledRowContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
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};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
`;
|
||||
|
||||
const StyledControlLabel = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export type SubMatchingSelectRowLeftSelectProps<T> = {
|
||||
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<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>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,75 @@
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
|
||||
import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions';
|
||||
|
||||
import { SubMatchingSelectDropdownButton } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectDropdownButton';
|
||||
import { SubMatchingSelectInput } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectInput';
|
||||
import {
|
||||
SpreadsheetMatchedSelectColumn,
|
||||
SpreadsheetMatchedSelectOptionsColumn,
|
||||
} from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetMatchedOptions } from '@/spreadsheet-import/types/SpreadsheetMatchedOptions';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import styled from '@emotion/styled';
|
||||
import { SelectOption } from 'twenty-ui/input';
|
||||
|
||||
const StyledDropdownContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
interface SubMatchingSelectRowRightDropdownProps<T> {
|
||||
option: SpreadsheetMatchedOptions<T> | Partial<SpreadsheetMatchedOptions<T>>;
|
||||
column:
|
||||
| SpreadsheetMatchedSelectColumn<T>
|
||||
| SpreadsheetMatchedSelectOptionsColumn<T>;
|
||||
onSubChange: (val: T, index: number, option: string) => void;
|
||||
placeholder: string;
|
||||
selectedOption?:
|
||||
| SpreadsheetMatchedOptions<T>
|
||||
| Partial<SpreadsheetMatchedOptions<T>>;
|
||||
}
|
||||
|
||||
export const SubMatchingSelectRowRightDropdown = <T extends string>({
|
||||
option,
|
||||
column,
|
||||
onSubChange,
|
||||
placeholder,
|
||||
}: SubMatchingSelectRowRightDropdownProps<T>) => {
|
||||
const dropdownId = `sub-matching-select-dropdown-${option.entry}`;
|
||||
|
||||
const { closeDropdown } = useDropdown(dropdownId);
|
||||
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
||||
const value = options.find((opt) => opt.value === option.value);
|
||||
|
||||
const handleSelect = (selectedOption: SelectOption) => {
|
||||
onSubChange(selectedOption.value as T, column.index, option.entry ?? '');
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledDropdownContainer>
|
||||
<Dropdown
|
||||
dropdownHotkeyScope={{ scope: dropdownId }}
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="bottom-start"
|
||||
clickableComponent={
|
||||
<SubMatchingSelectDropdownButton
|
||||
column={column}
|
||||
option={option}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<SubMatchingSelectInput
|
||||
defaultOption={value}
|
||||
options={options}
|
||||
onOptionSelected={handleSelect}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledDropdownContainer>
|
||||
);
|
||||
};
|
||||
@ -2,8 +2,8 @@ import styled from '@emotion/styled';
|
||||
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { IconForbid } from 'twenty-ui/display';
|
||||
@ -75,7 +75,7 @@ export const TemplateColumn = <T extends string>({
|
||||
value={isIgnored ? ignoreValue : selectValue}
|
||||
onChange={(value) => onChange(value?.value as T, column.index)}
|
||||
options={selectOptions}
|
||||
name={column.header}
|
||||
columnIndex={column.index.toString()}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { SubMatchingSelect } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelect';
|
||||
import { SubMatchingSelectRow } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/SubMatchingSelectRow';
|
||||
import { UnmatchColumnBanner } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumnBanner';
|
||||
import { SpreadsheetImportFields } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
@ -71,7 +71,7 @@ export const UnmatchColumn = <T extends string>({
|
||||
>
|
||||
<StyledContentWrapper>
|
||||
{column.matchedOptions.map((option) => (
|
||||
<SubMatchingSelect
|
||||
<SubMatchingSelectRow
|
||||
option={option}
|
||||
column={column}
|
||||
onSubChange={onSubChange}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Banner, IconChevronDown, IconInfoCircle } from 'twenty-ui/display';
|
||||
|
||||
const StyledBanner = styled(Banner)`
|
||||
@ -27,6 +28,16 @@ const StyledTransitionedIconChevronDown = styled(IconChevronDown)<{
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const StyledClickableContainer = styled.div`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const UnmatchColumnBanner = ({
|
||||
message,
|
||||
isExpanded,
|
||||
@ -37,16 +48,20 @@ export const UnmatchColumnBanner = ({
|
||||
buttonOnClick?: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledBanner>
|
||||
<IconInfoCircle color={theme.color.blue} size={theme.icon.size.md} />
|
||||
<StyledText>{message}</StyledText>
|
||||
{buttonOnClick && (
|
||||
<StyledTransitionedIconChevronDown
|
||||
isExpanded={isExpanded}
|
||||
onClick={buttonOnClick}
|
||||
size={theme.icon.size.md}
|
||||
/>
|
||||
{isDefined(buttonOnClick) ? (
|
||||
<StyledClickableContainer onClick={buttonOnClick}>
|
||||
<StyledText>{message}</StyledText>
|
||||
<StyledTransitionedIconChevronDown
|
||||
isExpanded={isExpanded}
|
||||
size={theme.icon.size.md}
|
||||
/>
|
||||
</StyledClickableContainer>
|
||||
) : (
|
||||
<StyledText>{message}</StyledText>
|
||||
)}
|
||||
</StyledBanner>
|
||||
);
|
||||
|
||||
@ -3,13 +3,13 @@ import styled from '@emotion/styled';
|
||||
import { Column, useRowSelection } from 'react-data-grid';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
||||
import {
|
||||
ImportedStructuredRow,
|
||||
SpreadsheetImportFields,
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ImportedStructuredRowMetadata } from '../types';
|
||||
import { AppTooltip } from 'twenty-ui/display';
|
||||
@ -148,6 +148,7 @@ export const generateColumns = <T extends string>(
|
||||
onRowChange({ ...row, [columnKey]: value?.value }, true);
|
||||
}}
|
||||
options={column.fieldType.options}
|
||||
columnIndex={column.key}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
@ -5,9 +5,10 @@ import { Key } from 'ts-key-enum';
|
||||
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
|
||||
import { DialogHotkeyScope } from '../types/DialogHotkeyScope';
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { DialogHotkeyScope } from '../types/DialogHotkeyScope';
|
||||
|
||||
const StyledDialogOverlay = styled(motion.div)`
|
||||
align-items: center;
|
||||
@ -19,7 +20,7 @@ const StyledDialogOverlay = styled(motion.div)`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
z-index: 9999;
|
||||
z-index: ${RootStackingContextZIndices.Dialog};
|
||||
`;
|
||||
|
||||
const StyledDialogContainer = styled(motion.div)`
|
||||
|
||||
@ -5,22 +5,23 @@ import { useSnackBarManagerScopedStates } from '@/ui/feedback/snack-bar-manager/
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { SnackBar } from './SnackBar';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||
|
||||
const StyledSnackBarContainer = styled.div`
|
||||
bottom: ${({ theme }) => theme.spacing(3)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
right: ${({ theme }) => theme.spacing(3)};
|
||||
bottom: ${({ theme }) => theme.spacing(3)};
|
||||
z-index: ${({ theme }) => theme.lastLayerZIndex};
|
||||
z-index: ${RootStackingContextZIndices.SnackBar};
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Please read this article to understand why we use this enum : https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Stacking_context
|
||||
*
|
||||
* It is important to keep track of the stacking contexts that are created on top of the root stacking context of the document.
|
||||
*
|
||||
* Right now we have to guess it by looking into the developer console
|
||||
*
|
||||
* This way we can avoid hazardous fidgeting with z-index CSS properties
|
||||
* and having to look down the tree in the developer console to see which component is in the root stacking context or not
|
||||
*
|
||||
* Using an enum enforces a single z-index for each component in the root stacking context
|
||||
*
|
||||
* TODO: add the other remaining components that can appear in the root stacking context
|
||||
*/
|
||||
export enum RootStackingContextZIndices {
|
||||
CommandMenu = 21,
|
||||
CommandMenuButton = 22,
|
||||
RootModalBackDrop = 39,
|
||||
RootModal = 40,
|
||||
DropdownPortal = 50,
|
||||
Dialog = 9999,
|
||||
SnackBar = 10002,
|
||||
NotFound = 10001,
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useInternalHotkeyScopeManagement } from '@/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement';
|
||||
@ -23,7 +24,7 @@ import { Key } from 'ts-key-enum';
|
||||
|
||||
export const StyledDropdownContentContainer = styled.div`
|
||||
display: flex;
|
||||
z-index: 30;
|
||||
z-index: ${RootStackingContextZIndices.DropdownPortal};
|
||||
`;
|
||||
|
||||
export type DropdownContentProps = {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
@ -31,7 +32,7 @@ const StyledModalDiv = styled(motion.div)<{
|
||||
}};
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
z-index: 10000; // should be higher than Backdrop's z-index
|
||||
z-index: ${RootStackingContextZIndices.RootModal}; // should be higher than Backdrop's z-index
|
||||
|
||||
width: ${({ isMobile, size, theme }) => {
|
||||
if (isMobile) return theme.modal.size.fullscreen;
|
||||
@ -109,7 +110,7 @@ const StyledBackDrop = styled(motion.div)<{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
z-index: ${RootStackingContextZIndices.RootModalBackDrop};
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { i18n } from '@lingui/core';
|
||||
@ -11,7 +12,7 @@ import { AppTooltip, TooltipDelay, TooltipPosition } from 'twenty-ui/display';
|
||||
import { getOsControlSymbol, useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledButtonWrapper = styled.div`
|
||||
z-index: 30;
|
||||
z-index: ${RootStackingContextZIndices.CommandMenuButton};
|
||||
`;
|
||||
|
||||
const StyledTooltipWrapper = styled.div`
|
||||
|
||||
@ -2,6 +2,7 @@ import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/S
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
@ -26,7 +27,7 @@ const StyledBackDrop = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 10000;
|
||||
z-index: ${RootStackingContextZIndices.NotFound};
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
|
||||
Reference in New Issue
Block a user