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:
@ -31,6 +31,7 @@
|
|||||||
"react-textarea-autosize": "^8.4.1",
|
"react-textarea-autosize": "^8.4.1",
|
||||||
"react-tooltip": "^5.13.1",
|
"react-tooltip": "^5.13.1",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
|
"scroll-into-view": "^1.16.2",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
@ -108,6 +109,7 @@
|
|||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
"@types/luxon": "^3.3.0",
|
"@types/luxon": "^3.3.0",
|
||||||
"@types/react-datepicker": "^4.11.2",
|
"@types/react-datepicker": "^4.11.2",
|
||||||
|
"@types/scroll-into-view": "^1.16.0",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||||
"babel-plugin-named-exports-order": "^0.0.2",
|
"babel-plugin-named-exports-order": "^0.0.2",
|
||||||
|
|||||||
@ -75,7 +75,6 @@ export const Board = ({
|
|||||||
[board, onUpdate],
|
[board, onUpdate],
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('board', board);
|
|
||||||
return (
|
return (
|
||||||
<StyledBoard>
|
<StyledBoard>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
|||||||
@ -1,112 +1,33 @@
|
|||||||
import { useState } from 'react';
|
import CompanyChip from '@/companies/components/CompanyChip';
|
||||||
import { v4 } from 'uuid';
|
import { EditableCellV2 } from '@/ui/components/editable-cell/EditableCellV2';
|
||||||
|
import { isCreateModeScopedState } from '@/ui/components/editable-cell/states/isCreateModeScopedState';
|
||||||
import CompanyChip, {
|
import { useRecoilScopedState } from '@/ui/hooks/useRecoilScopedState';
|
||||||
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 { getLogoUrlFromDomainName } from '@/utils/utils';
|
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
||||||
import {
|
import { Company, Person } from '~/generated/graphql';
|
||||||
Company,
|
|
||||||
Person,
|
|
||||||
QueryMode,
|
|
||||||
useInsertCompanyMutation,
|
|
||||||
useUpdatePeopleMutation,
|
|
||||||
} from '~/generated/graphql';
|
|
||||||
|
|
||||||
import { PeopleCompanyCreateCell } from './PeopleCompanyCreateCell';
|
import { PeopleCompanyCreateCell } from './PeopleCompanyCreateCell';
|
||||||
|
import { PeopleCompanyPicker } from './PeopleCompanyPicker';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
people: Pick<Person, 'id'> & {
|
people: Pick<Person, 'id'> & {
|
||||||
company?: Pick<Company, 'id' | 'name'> | null;
|
company?: Pick<Company, 'id' | 'name' | 'domainName'> | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PeopleCompanyCell({ people }: OwnProps) {
|
export function PeopleCompanyCell({ people }: OwnProps) {
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating] = useRecoilScopedState(isCreateModeScopedState);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isCreating ? (
|
return isCreating ? (
|
||||||
<PeopleCompanyCreateCell
|
<PeopleCompanyCreateCell people={people} />
|
||||||
initialCompanyName={initialCompanyName}
|
|
||||||
onCreate={handleCompanyCreate}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<EditableRelation<any, CompanyChipPropsType>
|
<EditableCellV2
|
||||||
relation={people.company}
|
editModeContent={<PeopleCompanyPicker people={people} />}
|
||||||
searchPlaceholder="Company"
|
nonEditModeContent={
|
||||||
ChipComponent={CompanyChip}
|
<CompanyChip
|
||||||
chipComponentPropsMapper={(company): CompanyChipPropsType => {
|
name={people.company?.name ?? ''}
|
||||||
return {
|
picture={getLogoUrlFromDomainName(people.company?.domainName)}
|
||||||
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
|
|
||||||
}
|
}
|
||||||
onCreate={() => {
|
|
||||||
setIsCreating(true);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,44 +1,90 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
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 { DoubleTextInput } from '@/ui/components/inputs/DoubleTextInput';
|
||||||
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
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 = {
|
type OwnProps = {
|
||||||
initialCompanyName: string;
|
people: Pick<Person, 'id'>;
|
||||||
onCreate: (companyName: string, companyDomainName: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PeopleCompanyCreateCell({
|
export function PeopleCompanyCreateCell({ people }: OwnProps) {
|
||||||
initialCompanyName,
|
const [, setIsCreating] = useRecoilScopedState(isCreateModeScopedState);
|
||||||
onCreate,
|
|
||||||
}: OwnProps) {
|
const [currentSearchFilter] = useRecoilScopedState(
|
||||||
const [companyName, setCompanyName] = useState(initialCompanyName);
|
relationPickerSearchFilterScopedState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [companyName, setCompanyName] = useState(currentSearchFilter);
|
||||||
|
|
||||||
const [companyDomainName, setCompanyDomainName] = useState('');
|
const [companyDomainName, setCompanyDomainName] = useState('');
|
||||||
|
const [insertCompany] = useInsertCompanyMutation();
|
||||||
|
const [updatePeople] = useUpdatePeopleMutation();
|
||||||
|
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
|
|
||||||
useListenClickOutsideArrayOfRef([containerRef], () => {
|
function handleDoubleTextChange(leftValue: string, rightValue: string): void {
|
||||||
onCreate(companyName, companyDomainName);
|
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(
|
useHotkeys(
|
||||||
'enter, escape',
|
'enter, escape',
|
||||||
() => {
|
() => {
|
||||||
onCreate(companyName, companyDomainName);
|
handleCompanyCreate(companyName, companyDomainName);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enableOnFormTags: true,
|
enableOnFormTags: true,
|
||||||
enableOnContentEditable: true,
|
enableOnContentEditable: true,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
[containerRef, companyName, companyDomainName, onCreate],
|
[companyName, companyDomainName, handleCompanyCreate],
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleDoubleTextChange(leftValue: string, rightValue: string): void {
|
useListenClickOutsideArrayOfRef([containerRef], () => {
|
||||||
setCompanyDomainName(leftValue);
|
handleCompanyCreate(companyName, companyDomainName);
|
||||||
setCompanyName(rightValue);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DoubleTextInput
|
<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 { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
@ -10,12 +10,17 @@ import { DropdownMenuButton } from './DropdownMenuButton';
|
|||||||
type Props = {
|
type Props = {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
hovered?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DropdownMenuSelectableItemContainer = styled(DropdownMenuButton)<Props>`
|
const DropdownMenuSelectableItemContainer = styled(DropdownMenuButton)<Props>`
|
||||||
${hoverBackground};
|
${hoverBackground};
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
background: ${(props) =>
|
||||||
|
props.hovered ? props.theme.lightBackgroundTransparent : 'transparent'};
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`;
|
`;
|
||||||
@ -35,10 +40,24 @@ export function DropdownMenuSelectableItem({
|
|||||||
selected,
|
selected,
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
|
hovered,
|
||||||
}: React.PropsWithChildren<Props>) {
|
}: React.PropsWithChildren<Props>) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hovered) {
|
||||||
|
window.scrollTo({
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [hovered]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuSelectableItemContainer onClick={onClick} selected={selected}>
|
<DropdownMenuSelectableItemContainer
|
||||||
|
onClick={onClick}
|
||||||
|
selected={selected}
|
||||||
|
hovered={hovered}
|
||||||
|
>
|
||||||
<StyledLeftContainer>{children}</StyledLeftContainer>
|
<StyledLeftContainer>{children}</StyledLeftContainer>
|
||||||
<StyledRightIcon>
|
<StyledRightIcon>
|
||||||
{selected && <IconCheck size={theme.iconSizeMedium} />}
|
{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));
|
||||||
|
}
|
||||||
@ -1,10 +1,15 @@
|
|||||||
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import type { Meta } from '@storybook/react';
|
import type { Meta } from '@storybook/react';
|
||||||
import { userEvent, within } from '@storybook/testing-library';
|
import { userEvent, within } from '@storybook/testing-library';
|
||||||
import { graphql } from 'msw';
|
import { graphql } from 'msw';
|
||||||
|
|
||||||
|
import { UPDATE_PERSON } from '@/people/services';
|
||||||
|
import { SEARCH_COMPANY_QUERY } from '@/search/services/search';
|
||||||
|
import { Company } from '~/generated/graphql';
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import { fetchOneFromData } from '~/testing/mock-data';
|
import { fetchOneFromData } from '~/testing/mock-data';
|
||||||
|
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||||
import { mockedPeopleData } from '~/testing/mock-data/people';
|
import { mockedPeopleData } from '~/testing/mock-data/people';
|
||||||
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
||||||
import { sleep } from '~/testing/sleep';
|
import { sleep } from '~/testing/sleep';
|
||||||
@ -95,59 +100,121 @@ export const CheckCheckboxes: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const editRelationMocks = (
|
||||||
|
initiallySelectedCompanyName: string,
|
||||||
|
searchCompanyNames: Array<string>,
|
||||||
|
updateSelectedCompany: Pick<Company, 'name' | 'domainName'>,
|
||||||
|
) => [
|
||||||
|
...graphqlMocks.filter((graphqlMock) => {
|
||||||
|
if (
|
||||||
|
typeof graphqlMock.info.operationName === 'string' &&
|
||||||
|
[
|
||||||
|
getOperationName(UPDATE_PERSON),
|
||||||
|
getOperationName(SEARCH_COMPANY_QUERY),
|
||||||
|
].includes(graphqlMock.info.operationName)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
...[
|
||||||
|
graphql.mutation(getOperationName(UPDATE_PERSON) ?? '', (req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.data({
|
||||||
|
updateOnePerson: {
|
||||||
|
...fetchOneFromData(mockedPeopleData, req.variables.id),
|
||||||
|
...{
|
||||||
|
company: {
|
||||||
|
id: req.variables.companyId,
|
||||||
|
name: updateSelectedCompany.name,
|
||||||
|
domainName: updateSelectedCompany.domainName,
|
||||||
|
__typename: 'Company',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
graphql.query(
|
||||||
|
getOperationName(SEARCH_COMPANY_QUERY) ?? '',
|
||||||
|
(req, res, ctx) => {
|
||||||
|
if (!req.variables.where?.AND) {
|
||||||
|
// Selected company case
|
||||||
|
const searchResults = mockedCompaniesData.filter((company) =>
|
||||||
|
[initiallySelectedCompanyName].includes(company.name),
|
||||||
|
);
|
||||||
|
return res(
|
||||||
|
ctx.data({
|
||||||
|
searchResults: searchResults,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.variables.where?.AND?.some(
|
||||||
|
(where: { id?: { in: Array<string> } }) => where.id?.in,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Selected company case
|
||||||
|
const searchResults = mockedCompaniesData.filter((company) =>
|
||||||
|
[initiallySelectedCompanyName].includes(company.name),
|
||||||
|
);
|
||||||
|
return res(
|
||||||
|
ctx.data({
|
||||||
|
searchResults: searchResults,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Search case
|
||||||
|
|
||||||
|
const searchResults = mockedCompaniesData.filter((company) =>
|
||||||
|
searchCompanyNames.includes(company.name),
|
||||||
|
);
|
||||||
|
return res(
|
||||||
|
ctx.data({
|
||||||
|
searchResults: searchResults,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
export const EditRelation: Story = {
|
export const EditRelation: Story = {
|
||||||
render: getRenderWrapperForPage(<People />, '/people'),
|
render: getRenderWrapperForPage(<People />, '/people'),
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const secondRowCompanyCell = await canvas.findByText(
|
const firstRowCompanyCell = await canvas.findByText(
|
||||||
mockedPeopleData[1].company.name,
|
mockedPeopleData[1].company.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
await userEvent.click(secondRowCompanyCell);
|
await userEvent.click(firstRowCompanyCell);
|
||||||
|
|
||||||
const relationInput = await canvas.findByPlaceholderText('Company');
|
const relationInput = await canvas.findByPlaceholderText('Search');
|
||||||
|
|
||||||
await userEvent.type(relationInput, 'Air', {
|
await userEvent.type(relationInput, 'Air', {
|
||||||
delay: 200,
|
delay: 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
const airbnbChip = await canvas.findByText('Airbnb', {
|
const airbnbChip = await canvas.findByText('Airbnb', {
|
||||||
selector: 'div > span',
|
selector: 'div',
|
||||||
});
|
});
|
||||||
|
|
||||||
await userEvent.click(airbnbChip);
|
await userEvent.click(airbnbChip);
|
||||||
|
|
||||||
const newSecondRowCompanyCell = await canvas.findByText('Airbnb');
|
const otherCell = await canvas.findByText('Janice Dane');
|
||||||
|
await userEvent.click(otherCell);
|
||||||
|
|
||||||
await userEvent.click(newSecondRowCompanyCell);
|
await canvas.findByText('Airbnb');
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
actions: {},
|
actions: {},
|
||||||
msw: [
|
msw: editRelationMocks('Qonto', ['Airbnb', 'Aircall'], {
|
||||||
...graphqlMocks.filter((graphqlMock) => {
|
name: 'Airbnb',
|
||||||
return graphqlMock.info.operationName !== 'UpdatePeople';
|
domainName: 'airbnb.com',
|
||||||
}),
|
}),
|
||||||
...[
|
|
||||||
graphql.mutation('UpdatePeople', (req, res, ctx) => {
|
|
||||||
return res(
|
|
||||||
ctx.data({
|
|
||||||
updateOnePerson: {
|
|
||||||
...fetchOneFromData(mockedPeopleData, req.variables.id),
|
|
||||||
...{
|
|
||||||
company: {
|
|
||||||
id: req.variables.companyId,
|
|
||||||
name: 'Airbnb',
|
|
||||||
domainName: 'airbnb.com',
|
|
||||||
__typename: 'Company',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -157,51 +224,32 @@ export const SelectRelationWithKeys: Story = {
|
|||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const thirdRowCompanyCell = await canvas.findByText(
|
const thirdRowCompanyCell = await canvas.findByText(
|
||||||
mockedPeopleData[2].company.name,
|
mockedPeopleData[0].company.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
await userEvent.click(thirdRowCompanyCell);
|
await userEvent.click(thirdRowCompanyCell);
|
||||||
|
|
||||||
const relationInput = await canvas.findByPlaceholderText('Company');
|
const relationInput = await canvas.findByPlaceholderText('Search');
|
||||||
|
|
||||||
await userEvent.type(relationInput, 'Air', {
|
await userEvent.type(relationInput, 'Air', {
|
||||||
delay: 200,
|
delay: 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
await userEvent.type(relationInput, '{arrowdown}');
|
|
||||||
await userEvent.type(relationInput, '{arrowdown}');
|
await userEvent.type(relationInput, '{arrowdown}');
|
||||||
await userEvent.type(relationInput, '{arrowup}');
|
await userEvent.type(relationInput, '{arrowup}');
|
||||||
await userEvent.type(relationInput, '{arrowdown}');
|
await userEvent.type(relationInput, '{arrowdown}');
|
||||||
|
await userEvent.type(relationInput, '{arrowdown}');
|
||||||
await userEvent.type(relationInput, '{enter}');
|
await userEvent.type(relationInput, '{enter}');
|
||||||
|
sleep(25);
|
||||||
|
|
||||||
const newThirdRowCompanyCell = await canvas.findByText('Aircall');
|
const allAirbns = await canvas.findAllByText('Aircall');
|
||||||
await userEvent.click(newThirdRowCompanyCell);
|
expect(allAirbns.length).toBe(1);
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
actions: {},
|
actions: {},
|
||||||
msw: [
|
msw: editRelationMocks('Qonto', ['Airbnb', 'Aircall'], {
|
||||||
...graphqlMocks.filter((graphqlMock) => {
|
name: 'Aircall',
|
||||||
return graphqlMock.info.operationName !== 'UpdatePeople';
|
domainName: 'aircall.io',
|
||||||
}),
|
}),
|
||||||
...[
|
|
||||||
graphql.mutation('UpdatePeople', (req, res, ctx) => {
|
|
||||||
return res(
|
|
||||||
ctx.data({
|
|
||||||
updateOnePerson: {
|
|
||||||
...fetchOneFromData(mockedPeopleData, req.variables.id),
|
|
||||||
...{
|
|
||||||
company: {
|
|
||||||
id: req.variables.companyId,
|
|
||||||
name: 'Aircall',
|
|
||||||
domainName: 'aircall.io',
|
|
||||||
__typename: 'Company',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { EditableDate } from '@/ui/components/editable-cell/types/EditableDate';
|
|||||||
import { EditablePhone } from '@/ui/components/editable-cell/types/EditablePhone';
|
import { EditablePhone } from '@/ui/components/editable-cell/types/EditablePhone';
|
||||||
import { EditableText } from '@/ui/components/editable-cell/types/EditableText';
|
import { EditableText } from '@/ui/components/editable-cell/types/EditableText';
|
||||||
import { ColumnHead } from '@/ui/components/table/ColumnHead';
|
import { ColumnHead } from '@/ui/components/table/ColumnHead';
|
||||||
|
import { RecoilScope } from '@/ui/hooks/RecoilScope';
|
||||||
import {
|
import {
|
||||||
IconBuildingSkyscraper,
|
IconBuildingSkyscraper,
|
||||||
IconCalendarEvent,
|
IconCalendarEvent,
|
||||||
@ -79,7 +80,11 @@ export const usePeopleColumns = () => {
|
|||||||
viewIcon={<IconBuildingSkyscraper size={16} />}
|
viewIcon={<IconBuildingSkyscraper size={16} />}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
cell: (props) => <PeopleCompanyCell people={props.row.original} />,
|
cell: (props) => (
|
||||||
|
<RecoilScope>
|
||||||
|
<PeopleCompanyCell people={props.row.original} />
|
||||||
|
</RecoilScope>
|
||||||
|
),
|
||||||
size: 150,
|
size: 150,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('phone', {
|
columnHelper.accessor('phone', {
|
||||||
|
|||||||
@ -45,7 +45,9 @@ export const graphqlMocks = [
|
|||||||
>(
|
>(
|
||||||
mockedCompaniesData,
|
mockedCompaniesData,
|
||||||
req.variables.where,
|
req.variables.where,
|
||||||
req.variables.orderBy,
|
Array.isArray(req.variables.orderBy)
|
||||||
|
? req.variables.orderBy
|
||||||
|
: [req.variables.orderBy],
|
||||||
req.variables.limit,
|
req.variables.limit,
|
||||||
);
|
);
|
||||||
return res(
|
return res(
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export function filterAndSortData<DataT>(
|
|||||||
filteredData = filterData<DataT>(data, where);
|
filteredData = filterData<DataT>(data, where);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orderBy) {
|
if (orderBy && Array.isArray(orderBy) && orderBy.length > 0 && orderBy[0]) {
|
||||||
const firstOrderBy = orderBy[0];
|
const firstOrderBy = orderBy[0];
|
||||||
|
|
||||||
const key = Object.keys(firstOrderBy)[0];
|
const key = Object.keys(firstOrderBy)[0];
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export const mockedPeopleData: Array<MockedPerson> = [
|
|||||||
lastname: 'Prot',
|
lastname: 'Prot',
|
||||||
email: 'alexandre@qonto.com',
|
email: 'alexandre@qonto.com',
|
||||||
company: {
|
company: {
|
||||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6c',
|
id: '5c21e19e-e049-4393-8c09-3e3f8fb09ecb',
|
||||||
name: 'Qonto',
|
name: 'Qonto',
|
||||||
domainName: 'qonto.com',
|
domainName: 'qonto.com',
|
||||||
__typename: 'Company',
|
__typename: 'Company',
|
||||||
|
|||||||
@ -5070,6 +5070,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||||
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
|
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
|
||||||
|
|
||||||
|
"@types/scroll-into-view@^1.16.0":
|
||||||
|
version "1.16.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/scroll-into-view/-/scroll-into-view-1.16.0.tgz#bd02094307624cc0243c34dca478510a0a8515e8"
|
||||||
|
integrity sha512-WT0YBP7CLi3XH/gDbWdtBf4mQVyE7zQrpEZ2rNrxz9tVoIPJf97zGlfRqnkECj7P8rPkFxVlo1KbOOMetcchdA==
|
||||||
|
|
||||||
"@types/semver@^7.3.12", "@types/semver@^7.3.4":
|
"@types/semver@^7.3.12", "@types/semver@^7.3.4":
|
||||||
version "7.5.0"
|
version "7.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a"
|
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a"
|
||||||
@ -15137,6 +15142,11 @@ schema-utils@^4.0.0:
|
|||||||
ajv-formats "^2.1.1"
|
ajv-formats "^2.1.1"
|
||||||
ajv-keywords "^5.1.0"
|
ajv-keywords "^5.1.0"
|
||||||
|
|
||||||
|
scroll-into-view@^1.16.2:
|
||||||
|
version "1.16.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/scroll-into-view/-/scroll-into-view-1.16.2.tgz#ea3e810dacc861fb9c115eac7bf603e564f0104a"
|
||||||
|
integrity sha512-vyTE0i27o6eldt9xinjHec41Dw05y+faoI+s2zNKJAVOdbA5M2XZrYq/obJ8E+QDQulJ2gDjgui9w9m9RZSRng==
|
||||||
|
|
||||||
scuid@^1.1.0:
|
scuid@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/scuid/-/scuid-1.1.0.tgz#d3f9f920956e737a60f72d0e4ad280bf324d5dab"
|
resolved "https://registry.yarnpkg.com/scuid/-/scuid-1.1.0.tgz#d3f9f920956e737a60f72d0e4ad280bf324d5dab"
|
||||||
|
|||||||
Reference in New Issue
Block a user