Feat/single entity select relation picker (#345)
* - Implemented recoil scoped state - Implemented SingleEntitySelect - Implemented keyboard shortcut up/down select * Added useRecoilScopedValue * Fix storybook * Fix storybook * Fix storybook * Fix storybook --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -75,7 +75,6 @@ export const Board = ({
|
||||
[board, onUpdate],
|
||||
);
|
||||
|
||||
console.log('board', board);
|
||||
return (
|
||||
<StyledBoard>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
|
||||
@ -1,112 +1,33 @@
|
||||
import { useState } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import CompanyChip, {
|
||||
CompanyChipPropsType,
|
||||
} from '@/companies/components/CompanyChip';
|
||||
import { SearchConfigType } from '@/search/interfaces/interface';
|
||||
import { SEARCH_COMPANY_QUERY } from '@/search/services/search';
|
||||
import { EditableRelation } from '@/ui/components/editable-cell/types/EditableRelation';
|
||||
import { logError } from '@/utils/logs/logError';
|
||||
import CompanyChip from '@/companies/components/CompanyChip';
|
||||
import { EditableCellV2 } from '@/ui/components/editable-cell/EditableCellV2';
|
||||
import { isCreateModeScopedState } from '@/ui/components/editable-cell/states/isCreateModeScopedState';
|
||||
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
|
||||
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
||||
import {
|
||||
Company,
|
||||
Person,
|
||||
QueryMode,
|
||||
useInsertCompanyMutation,
|
||||
useUpdatePeopleMutation,
|
||||
} from '~/generated/graphql';
|
||||
import { Company, Person } from '~/generated/graphql';
|
||||
|
||||
import { PeopleCompanyCreateCell } from './PeopleCompanyCreateCell';
|
||||
import { PeopleCompanyPicker } from './PeopleCompanyPicker';
|
||||
|
||||
export type OwnProps = {
|
||||
people: Pick<Person, 'id'> & {
|
||||
company?: Pick<Company, 'id' | 'name'> | null;
|
||||
company?: Pick<Company, 'id' | 'name' | 'domainName'> | null;
|
||||
};
|
||||
};
|
||||
|
||||
export function PeopleCompanyCell({ people }: OwnProps) {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [insertCompany] = useInsertCompanyMutation();
|
||||
const [updatePeople] = useUpdatePeopleMutation();
|
||||
const [initialCompanyName, setInitialCompanyName] = useState('');
|
||||
|
||||
async function handleCompanyCreate(
|
||||
companyName: string,
|
||||
companyDomainName: string,
|
||||
) {
|
||||
const newCompanyId = v4();
|
||||
|
||||
try {
|
||||
await insertCompany({
|
||||
variables: {
|
||||
id: newCompanyId,
|
||||
name: companyName,
|
||||
domainName: companyDomainName,
|
||||
address: '',
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
await updatePeople({
|
||||
variables: {
|
||||
...people,
|
||||
companyId: newCompanyId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// TODO: handle error better
|
||||
logError(error);
|
||||
}
|
||||
|
||||
setIsCreating(false);
|
||||
}
|
||||
|
||||
// TODO: should be replaced with search context
|
||||
function handleChangeSearchInput(searchInput: string) {
|
||||
setInitialCompanyName(searchInput);
|
||||
}
|
||||
const [isCreating] = useRecoilScopedState(isCreateModeScopedState);
|
||||
|
||||
return isCreating ? (
|
||||
<PeopleCompanyCreateCell
|
||||
initialCompanyName={initialCompanyName}
|
||||
onCreate={handleCompanyCreate}
|
||||
/>
|
||||
<PeopleCompanyCreateCell people={people} />
|
||||
) : (
|
||||
<EditableRelation<any, CompanyChipPropsType>
|
||||
relation={people.company}
|
||||
searchPlaceholder="Company"
|
||||
ChipComponent={CompanyChip}
|
||||
chipComponentPropsMapper={(company): CompanyChipPropsType => {
|
||||
return {
|
||||
name: company.name || '',
|
||||
picture: getLogoUrlFromDomainName(company.domainName),
|
||||
};
|
||||
}}
|
||||
onChange={async (relation) => {
|
||||
await updatePeople({
|
||||
variables: {
|
||||
...people,
|
||||
companyId: relation.id,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onChangeSearchInput={handleChangeSearchInput}
|
||||
searchConfig={
|
||||
{
|
||||
query: SEARCH_COMPANY_QUERY,
|
||||
template: (searchInput: string) => ({
|
||||
name: { contains: `%${searchInput}%`, mode: QueryMode.Insensitive },
|
||||
}),
|
||||
resultMapper: (company) => ({
|
||||
render: (company: any) => company.name,
|
||||
value: company,
|
||||
}),
|
||||
} satisfies SearchConfigType
|
||||
<EditableCellV2
|
||||
editModeContent={<PeopleCompanyPicker people={people} />}
|
||||
nonEditModeContent={
|
||||
<CompanyChip
|
||||
name={people.company?.name ?? ''}
|
||||
picture={getLogoUrlFromDomainName(people.company?.domainName)}
|
||||
/>
|
||||
}
|
||||
onCreate={() => {
|
||||
setIsCreating(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,44 +1,90 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/relationPickerSearchFilterScopedState';
|
||||
import { isCreateModeScopedState } from '@/ui/components/editable-cell/states/isCreateModeScopedState';
|
||||
import { DoubleTextInput } from '@/ui/components/inputs/DoubleTextInput';
|
||||
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
|
||||
import { logError } from '@/utils/logs/logError';
|
||||
import {
|
||||
Person,
|
||||
useInsertCompanyMutation,
|
||||
useUpdatePeopleMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
type OwnProps = {
|
||||
initialCompanyName: string;
|
||||
onCreate: (companyName: string, companyDomainName: string) => void;
|
||||
people: Pick<Person, 'id'>;
|
||||
};
|
||||
|
||||
export function PeopleCompanyCreateCell({
|
||||
initialCompanyName,
|
||||
onCreate,
|
||||
}: OwnProps) {
|
||||
const [companyName, setCompanyName] = useState(initialCompanyName);
|
||||
export function PeopleCompanyCreateCell({ people }: OwnProps) {
|
||||
const [, setIsCreating] = useRecoilScopedState(isCreateModeScopedState);
|
||||
|
||||
const [currentSearchFilter] = useRecoilScopedState(
|
||||
relationPickerSearchFilterScopedState,
|
||||
);
|
||||
|
||||
const [companyName, setCompanyName] = useState(currentSearchFilter);
|
||||
|
||||
const [companyDomainName, setCompanyDomainName] = useState('');
|
||||
const [insertCompany] = useInsertCompanyMutation();
|
||||
const [updatePeople] = useUpdatePeopleMutation();
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
useListenClickOutsideArrayOfRef([containerRef], () => {
|
||||
onCreate(companyName, companyDomainName);
|
||||
});
|
||||
function handleDoubleTextChange(leftValue: string, rightValue: string): void {
|
||||
setCompanyDomainName(leftValue);
|
||||
setCompanyName(rightValue);
|
||||
}
|
||||
|
||||
async function handleCompanyCreate(
|
||||
companyName: string,
|
||||
companyDomainName: string,
|
||||
) {
|
||||
const newCompanyId = v4();
|
||||
|
||||
try {
|
||||
await insertCompany({
|
||||
variables: {
|
||||
id: newCompanyId,
|
||||
name: companyName,
|
||||
domainName: companyDomainName,
|
||||
address: '',
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
await updatePeople({
|
||||
variables: {
|
||||
...people,
|
||||
companyId: newCompanyId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// TODO: handle error better
|
||||
logError(error);
|
||||
}
|
||||
|
||||
setIsCreating(false);
|
||||
}
|
||||
|
||||
useHotkeys(
|
||||
'enter, escape',
|
||||
() => {
|
||||
onCreate(companyName, companyDomainName);
|
||||
handleCompanyCreate(companyName, companyDomainName);
|
||||
},
|
||||
{
|
||||
enableOnFormTags: true,
|
||||
enableOnContentEditable: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
[containerRef, companyName, companyDomainName, onCreate],
|
||||
[companyName, companyDomainName, handleCompanyCreate],
|
||||
);
|
||||
|
||||
function handleDoubleTextChange(leftValue: string, rightValue: string): void {
|
||||
setCompanyDomainName(leftValue);
|
||||
setCompanyName(rightValue);
|
||||
}
|
||||
useListenClickOutsideArrayOfRef([containerRef], () => {
|
||||
handleCompanyCreate(companyName, companyDomainName);
|
||||
});
|
||||
|
||||
return (
|
||||
<DoubleTextInput
|
||||
|
||||
73
front/src/modules/people/components/PeopleCompanyPicker.tsx
Normal file
73
front/src/modules/people/components/PeopleCompanyPicker.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { SingleEntitySelect } from '@/relation-picker/components/SingleEntitySelect';
|
||||
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
|
||||
import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/relationPickerSearchFilterScopedState';
|
||||
import { isCreateModeScopedState } from '@/ui/components/editable-cell/states/isCreateModeScopedState';
|
||||
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
|
||||
import { isSomeInputInEditModeState } from '@/ui/tables/states/isSomeInputInEditModeState';
|
||||
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
||||
import {
|
||||
CommentableType,
|
||||
Company,
|
||||
Person,
|
||||
useSearchCompanyQuery,
|
||||
useUpdatePeopleMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export type OwnProps = {
|
||||
people: Pick<Person, 'id'> & { company?: Pick<Company, 'id'> | null };
|
||||
};
|
||||
|
||||
export function PeopleCompanyPicker({ people }: OwnProps) {
|
||||
const [, setIsCreating] = useRecoilScopedState(isCreateModeScopedState);
|
||||
|
||||
const [searchFilter] = useRecoilScopedState(
|
||||
relationPickerSearchFilterScopedState,
|
||||
);
|
||||
const [updatePeople] = useUpdatePeopleMutation();
|
||||
const [, setIsSomeInputInEditMode] = useRecoilState(
|
||||
isSomeInputInEditModeState,
|
||||
);
|
||||
|
||||
const companies = useFilteredSearchEntityQuery({
|
||||
queryHook: useSearchCompanyQuery,
|
||||
selectedIds: [people.company?.id ?? ''],
|
||||
searchFilter: searchFilter,
|
||||
mappingFunction: (company) => ({
|
||||
entityType: CommentableType.Company,
|
||||
id: company.id,
|
||||
name: company.name,
|
||||
avatarType: 'squared',
|
||||
avatarUrl: getLogoUrlFromDomainName(company.domainName),
|
||||
}),
|
||||
orderByField: 'name',
|
||||
searchOnFields: ['name'],
|
||||
});
|
||||
|
||||
async function handleEntitySelected(entity: any) {
|
||||
setIsSomeInputInEditMode(false);
|
||||
|
||||
await updatePeople({
|
||||
variables: {
|
||||
...people,
|
||||
companyId: entity.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
setIsCreating(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<SingleEntitySelect
|
||||
onCreate={handleCreate}
|
||||
onEntitySelected={handleEntitySelected}
|
||||
entities={{
|
||||
entitiesToSelect: companies.entitiesToSelect,
|
||||
selectedEntity: companies.selectedEntities[0],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
import { useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { IconPlus } from '@tabler/icons-react';
|
||||
|
||||
import { EntityForSelect } from '@/relation-picker/types/EntityForSelect';
|
||||
import { DropdownMenu } from '@/ui/components/menu/DropdownMenu';
|
||||
import { DropdownMenuButton } from '@/ui/components/menu/DropdownMenuButton';
|
||||
import { DropdownMenuItem } from '@/ui/components/menu/DropdownMenuItem';
|
||||
import { DropdownMenuItemContainer } from '@/ui/components/menu/DropdownMenuItemContainer';
|
||||
import { DropdownMenuSearch } from '@/ui/components/menu/DropdownMenuSearch';
|
||||
import { DropdownMenuSelectableItem } from '@/ui/components/menu/DropdownMenuSelectableItem';
|
||||
import { DropdownMenuSeparator } from '@/ui/components/menu/DropdownMenuSeparator';
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
import { isDefined } from '@/utils/type-guards/isDefined';
|
||||
|
||||
import { useEntitySelectLogic } from '../hooks/useEntitySelectLogic';
|
||||
|
||||
export type EntitiesForSingleEntitySelect<
|
||||
CustomEntityForSelect extends EntityForSelect,
|
||||
> = {
|
||||
selectedEntity: CustomEntityForSelect;
|
||||
entitiesToSelect: CustomEntityForSelect[];
|
||||
};
|
||||
|
||||
export function SingleEntitySelect<
|
||||
CustomEntityForSelect extends EntityForSelect,
|
||||
>({
|
||||
entities,
|
||||
onEntitySelected,
|
||||
onCreate,
|
||||
}: {
|
||||
onCreate?: () => void;
|
||||
entities: EntitiesForSingleEntitySelect<CustomEntityForSelect>;
|
||||
onEntitySelected: (entity: CustomEntityForSelect) => void;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const entitiesInDropdown = isDefined(entities.selectedEntity)
|
||||
? [entities.selectedEntity, ...(entities.entitiesToSelect ?? [])]
|
||||
: entities.entitiesToSelect ?? [];
|
||||
|
||||
const { hoveredIndex, searchFilter, handleSearchFilterChange } =
|
||||
useEntitySelectLogic({
|
||||
entities: entitiesInDropdown,
|
||||
containerRef,
|
||||
});
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
onEntitySelected(entitiesInDropdown[hoveredIndex]);
|
||||
},
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[entitiesInDropdown, hoveredIndex, onEntitySelected],
|
||||
);
|
||||
|
||||
const showCreateButton = isDefined(onCreate) && searchFilter !== '';
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuSearch
|
||||
value={searchFilter}
|
||||
onChange={handleSearchFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
{showCreateButton && (
|
||||
<>
|
||||
<DropdownMenuItemContainer>
|
||||
<DropdownMenuButton onClick={onCreate}>
|
||||
<IconPlus size={theme.iconSizeMedium} />
|
||||
Create new
|
||||
</DropdownMenuButton>
|
||||
</DropdownMenuItemContainer>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItemContainer ref={containerRef}>
|
||||
{entitiesInDropdown?.map((entity, index) => (
|
||||
<DropdownMenuSelectableItem
|
||||
key={entity.id}
|
||||
selected={entities.selectedEntity?.id === entity.id}
|
||||
hovered={hoveredIndex === index}
|
||||
onClick={() => onEntitySelected(entity)}
|
||||
>
|
||||
<Avatar
|
||||
avatarUrl={entity.avatarUrl}
|
||||
placeholder={entity.name}
|
||||
size={16}
|
||||
type={entity.avatarType ?? 'rounded'}
|
||||
/>
|
||||
{entity.name}
|
||||
</DropdownMenuSelectableItem>
|
||||
))}
|
||||
{entitiesInDropdown?.length === 0 && (
|
||||
<DropdownMenuItem>No result</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuItemContainer>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
104
front/src/modules/relation-picker/hooks/useEntitySelectLogic.ts
Normal file
104
front/src/modules/relation-picker/hooks/useEntitySelectLogic.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { debounce } from 'lodash';
|
||||
import scrollIntoView from 'scroll-into-view';
|
||||
|
||||
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
|
||||
|
||||
import { relationPickerSearchFilterScopedState } from '../states/relationPickerSearchFilterScopedState';
|
||||
import { EntityForSelect } from '../types/EntityForSelect';
|
||||
|
||||
export function useEntitySelectLogic<
|
||||
CustomEntityForSelect extends EntityForSelect,
|
||||
>({
|
||||
containerRef,
|
||||
entities,
|
||||
}: {
|
||||
entities: CustomEntityForSelect[];
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
}) {
|
||||
const [hoveredIndex, setHoveredIndex] = useState(0);
|
||||
|
||||
const [searchFilter, setSearchFilter] = useRecoilScopedState(
|
||||
relationPickerSearchFilterScopedState,
|
||||
);
|
||||
|
||||
const debouncedSetSearchFilter = debounce(setSearchFilter, 100, {
|
||||
leading: true,
|
||||
});
|
||||
|
||||
function handleSearchFilterChange(
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) {
|
||||
debouncedSetSearchFilter(event.currentTarget.value);
|
||||
setHoveredIndex(0);
|
||||
}
|
||||
|
||||
useHotkeys(
|
||||
'down',
|
||||
() => {
|
||||
setHoveredIndex((prevSelectedIndex) =>
|
||||
Math.min(prevSelectedIndex + 1, (entities?.length ?? 0) - 1),
|
||||
);
|
||||
|
||||
const currentHoveredRef = containerRef.current?.children[
|
||||
hoveredIndex
|
||||
] as HTMLElement;
|
||||
|
||||
if (currentHoveredRef) {
|
||||
scrollIntoView(currentHoveredRef, {
|
||||
align: {
|
||||
top: 0.275,
|
||||
},
|
||||
isScrollable: (target) => {
|
||||
return target === containerRef.current;
|
||||
},
|
||||
time: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
[setHoveredIndex, entities],
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'up',
|
||||
() => {
|
||||
setHoveredIndex((prevSelectedIndex) =>
|
||||
Math.max(prevSelectedIndex - 1, 0),
|
||||
);
|
||||
|
||||
const currentHoveredRef = containerRef.current?.children[
|
||||
hoveredIndex
|
||||
] as HTMLElement;
|
||||
|
||||
if (currentHoveredRef) {
|
||||
scrollIntoView(currentHoveredRef, {
|
||||
align: {
|
||||
top: 0.5,
|
||||
},
|
||||
isScrollable: (target) => {
|
||||
return target === containerRef.current;
|
||||
},
|
||||
time: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
preventDefault: true,
|
||||
},
|
||||
[setHoveredIndex, entities],
|
||||
);
|
||||
|
||||
return {
|
||||
hoveredIndex,
|
||||
searchFilter,
|
||||
handleSearchFilterChange,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const relationPickerSearchFilterScopedState = atomFamily<string, string>(
|
||||
{
|
||||
key: 'relationPickerSearchFilterScopedState',
|
||||
default: '',
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,71 @@
|
||||
import { ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
|
||||
|
||||
import { isSomeInputInEditModeState } from '../../tables/states/isSomeInputInEditModeState';
|
||||
|
||||
import { isEditModeScopedState } from './states/isEditModeScopedState';
|
||||
import { EditableCellDisplayMode } from './EditableCellDisplayMode';
|
||||
import { EditableCellEditMode } from './EditableCellEditMode';
|
||||
|
||||
export const CellBaseContainer = styled.div`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
editModeContent: ReactElement;
|
||||
nonEditModeContent: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
};
|
||||
|
||||
export function EditableCellV2({
|
||||
editModeHorizontalAlign = 'left',
|
||||
editModeVerticalPosition = 'over',
|
||||
editModeContent,
|
||||
nonEditModeContent,
|
||||
}: OwnProps) {
|
||||
const [isEditMode, setIsEditMode] = useRecoilScopedState(
|
||||
isEditModeScopedState,
|
||||
);
|
||||
const [isSomeInputInEditMode, setIsSomeInputInEditMode] = useRecoilState(
|
||||
isSomeInputInEditModeState,
|
||||
);
|
||||
|
||||
function handleOnClick() {
|
||||
if (!isSomeInputInEditMode) {
|
||||
setIsSomeInputInEditMode(true);
|
||||
setIsEditMode(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleOnOutsideClick() {
|
||||
setIsEditMode(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<CellBaseContainer onClick={handleOnClick}>
|
||||
{isEditMode ? (
|
||||
<EditableCellEditMode
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={handleOnOutsideClick}
|
||||
>
|
||||
{editModeContent}
|
||||
</EditableCellEditMode>
|
||||
) : (
|
||||
<EditableCellDisplayMode>{nonEditModeContent}</EditableCellDisplayMode>
|
||||
)}
|
||||
</CellBaseContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isCreateModeScopedState = atomFamily<boolean, string>({
|
||||
key: 'isCreateModeScopedState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isEditModeScopedState = atomFamily<boolean, string>({
|
||||
key: 'isEditModeScopedState',
|
||||
default: false,
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
@ -10,12 +10,17 @@ import { DropdownMenuButton } from './DropdownMenuButton';
|
||||
type Props = {
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
hovered?: boolean;
|
||||
};
|
||||
|
||||
const DropdownMenuSelectableItemContainer = styled(DropdownMenuButton)<Props>`
|
||||
${hoverBackground};
|
||||
|
||||
align-items: center;
|
||||
|
||||
background: ${(props) =>
|
||||
props.hovered ? props.theme.lightBackgroundTransparent : 'transparent'};
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
@ -35,10 +40,24 @@ export function DropdownMenuSelectableItem({
|
||||
selected,
|
||||
onClick,
|
||||
children,
|
||||
hovered,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (hovered) {
|
||||
window.scrollTo({
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}, [hovered]);
|
||||
|
||||
return (
|
||||
<DropdownMenuSelectableItemContainer onClick={onClick} selected={selected}>
|
||||
<DropdownMenuSelectableItemContainer
|
||||
onClick={onClick}
|
||||
selected={selected}
|
||||
hovered={hovered}
|
||||
>
|
||||
<StyledLeftContainer>{children}</StyledLeftContainer>
|
||||
<StyledRightIcon>
|
||||
{selected && <IconCheck size={theme.iconSizeMedium} />}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export function SelectSingleEntity() {
|
||||
return <></>;
|
||||
}
|
||||
14
front/src/modules/ui/hooks/RecoilScope.tsx
Normal file
14
front/src/modules/ui/hooks/RecoilScope.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { useState } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { RecoilScopeContext } from './RecoilScopeContext';
|
||||
|
||||
export function RecoilScope({ children }: { children: React.ReactNode }) {
|
||||
const [currentScopeId] = useState(v4());
|
||||
|
||||
return (
|
||||
<RecoilScopeContext.Provider value={currentScopeId}>
|
||||
{children}
|
||||
</RecoilScopeContext.Provider>
|
||||
);
|
||||
}
|
||||
3
front/src/modules/ui/hooks/RecoilScopeContext.ts
Normal file
3
front/src/modules/ui/hooks/RecoilScopeContext.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const RecoilScopeContext = createContext<string | null>(null);
|
||||
17
front/src/modules/ui/hooks/useRecoilScopedState.ts
Normal file
17
front/src/modules/ui/hooks/useRecoilScopedState.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { useContext } from 'react';
|
||||
import { RecoilState, useRecoilState } from 'recoil';
|
||||
|
||||
import { RecoilScopeContext } from './RecoilScopeContext';
|
||||
|
||||
export function useRecoilScopedState<T>(
|
||||
recoilState: (param: string) => RecoilState<T>,
|
||||
) {
|
||||
const recoilScopeId = useContext(RecoilScopeContext);
|
||||
|
||||
if (!recoilScopeId)
|
||||
throw new Error(
|
||||
`Using a scoped atom without a RecoilScope : ${recoilState('').key}`,
|
||||
);
|
||||
|
||||
return useRecoilState<T>(recoilState(recoilScopeId));
|
||||
}
|
||||
17
front/src/modules/ui/hooks/useRecoilScopedValue.ts
Normal file
17
front/src/modules/ui/hooks/useRecoilScopedValue.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { useContext } from 'react';
|
||||
import { RecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { RecoilScopeContext } from './RecoilScopeContext';
|
||||
|
||||
export function useRecoilScopedValue<T>(
|
||||
recoilState: (param: string) => RecoilState<T>,
|
||||
) {
|
||||
const recoilScopeId = useContext(RecoilScopeContext);
|
||||
|
||||
if (!recoilScopeId)
|
||||
throw new Error(
|
||||
`Using a scoped atom without a RecoilScope : ${recoilState('').key}`,
|
||||
);
|
||||
|
||||
return useRecoilValue<T>(recoilState(recoilScopeId));
|
||||
}
|
||||
Reference in New Issue
Block a user