Migrate to a monorepo structure (#2909)

This commit is contained in:
Charles Bochet
2023-12-10 18:10:54 +01:00
committed by GitHub
parent a70a9281eb
commit 5bdca9de6c
2304 changed files with 37152 additions and 25869 deletions

View File

@ -0,0 +1,106 @@
import { useRef } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import debounce from 'lodash.debounce';
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 { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { Avatar } from '@/users/components/Avatar';
import { EntityForSelect } from '../types/EntityForSelect';
export type EntitiesForMultipleEntitySelect<
CustomEntityForSelect extends EntityForSelect,
> = {
selectedEntities: CustomEntityForSelect[];
filteredSelectedEntities: CustomEntityForSelect[];
entitiesToSelect: CustomEntityForSelect[];
loading: boolean;
};
export const MultipleEntitySelect = <
CustomEntityForSelect extends EntityForSelect,
>({
entities,
onChange,
onSubmit,
onSearchFilterChange,
searchFilter,
value,
}: {
entities: EntitiesForMultipleEntitySelect<CustomEntityForSelect>;
searchFilter: string;
onSearchFilterChange: (newSearchFilter: string) => void;
onChange: (value: Record<string, boolean>) => void;
onCancel?: () => void;
onSubmit?: () => void;
value: Record<string, boolean>;
}) => {
const debouncedSetSearchFilter = debounce(onSearchFilterChange, 100, {
leading: true,
});
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
debouncedSetSearchFilter(event.currentTarget.value);
onSearchFilterChange(event.currentTarget.value);
};
let entitiesInDropdown = [
...(entities.filteredSelectedEntities ?? []),
...(entities.entitiesToSelect ?? []),
];
entitiesInDropdown = entitiesInDropdown.filter((entity) =>
isNonEmptyString(entity.name),
);
const containerRef = useRef<HTMLDivElement>(null);
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
onSubmit?.();
},
});
return (
<DropdownMenu ref={containerRef} data-select-disable>
<DropdownMenuSearchInput
value={searchFilter}
onChange={handleFilterChange}
autoFocus
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{entitiesInDropdown?.map((entity) => (
<MenuItemMultiSelectAvatar
key={entity.id}
selected={value[entity.id]}
onSelectChange={(newCheckedValue) =>
onChange({ ...value, [entity.id]: newCheckedValue })
}
avatar={
<Avatar
avatarUrl={entity.avatarUrl}
colorId={entity.id}
placeholder={entity.name}
size="md"
type={entity.avatarType ?? 'rounded'}
/>
}
text={entity.name}
/>
))}
{entitiesInDropdown?.length === 0 && <MenuItem text="No result" />}
</DropdownMenuItemsContainer>
</DropdownMenu>
);
};

View File

@ -0,0 +1,95 @@
import { useEffect } from 'react';
import { useQuery } from '@apollo/client';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { FieldDefinition } from '@/object-record/field/types/FieldDefinition';
import { FieldRelationMetadata } from '@/object-record/field/types/FieldMetadata';
import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { IconForbid } from '@/ui/display/icon';
export type RelationPickerProps = {
recordId?: string;
onSubmit: (newUser: EntityForSelect | null) => void;
onCancel?: () => void;
width?: number;
excludeRecordIds?: string[];
initialSearchFilter?: string | null;
fieldDefinition: FieldDefinition<FieldRelationMetadata>;
};
export const RelationPicker = ({
recordId,
onSubmit,
onCancel,
excludeRecordIds,
width,
initialSearchFilter,
fieldDefinition,
}: RelationPickerProps) => {
const { relationPickerSearchFilter, setRelationPickerSearchFilter } =
useRelationPicker();
useEffect(() => {
setRelationPickerSearchFilter(initialSearchFilter ?? '');
}, [initialSearchFilter, setRelationPickerSearchFilter]);
// TODO: refactor useFilteredSearchEntityQuery
const { findManyRecordsQuery } = useObjectMetadataItem({
objectNameSingular:
fieldDefinition.metadata.relationObjectMetadataNameSingular,
});
const useFindManyQuery = (options: any) =>
useQuery(findManyRecordsQuery, options);
const { identifiersMapper, searchQuery } = useRelationPicker();
const { objectNameSingular: relationObjectNameSingular } =
useObjectNameSingularFromPlural({
objectNamePlural:
fieldDefinition.metadata.relationObjectMetadataNamePlural,
});
const records = useFilteredSearchEntityQuery({
queryHook: useFindManyQuery,
filters: [
{
fieldNames:
searchQuery?.computeFilterFields?.(
fieldDefinition.metadata.relationObjectMetadataNameSingular,
) ?? [],
filter: relationPickerSearchFilter,
},
],
orderByField: 'createdAt',
mappingFunction: (record: any) =>
identifiersMapper?.(
record,
fieldDefinition.metadata.relationObjectMetadataNameSingular,
),
selectedIds: recordId ? [recordId] : [],
excludeEntityIds: excludeRecordIds,
objectNameSingular: relationObjectNameSingular,
});
const handleEntitySelected = async (selectedUser: any | null | undefined) => {
onSubmit(selectedUser ?? null);
};
return (
<SingleEntitySelect
EmptyIcon={IconForbid}
emptyLabel={'No ' + fieldDefinition.label}
entitiesToSelect={records.entitiesToSelect}
loading={records.loading}
onCancel={onCancel}
onEntitySelected={handleEntitySelected}
selectedEntity={records.selectedEntities[0]}
width={width}
/>
);
};

View File

@ -0,0 +1,91 @@
import { useRef } from 'react';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined';
import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch';
import { EntityForSelect } from '../types/EntityForSelect';
import {
SingleEntitySelectBase,
SingleEntitySelectBaseProps,
} from './SingleEntitySelectBase';
export type SingleEntitySelectProps<
CustomEntityForSelect extends EntityForSelect,
> = {
disableBackgroundBlur?: boolean;
onCreate?: () => void;
width?: number;
} & Pick<
SingleEntitySelectBaseProps<CustomEntityForSelect>,
| 'EmptyIcon'
| 'emptyLabel'
| 'entitiesToSelect'
| 'loading'
| 'onCancel'
| 'onEntitySelected'
| 'selectedEntity'
>;
export const SingleEntitySelect = <
CustomEntityForSelect extends EntityForSelect,
>({
EmptyIcon,
disableBackgroundBlur = false,
emptyLabel,
entitiesToSelect,
loading,
onCancel,
onCreate,
onEntitySelected,
selectedEntity,
width = 200,
}: SingleEntitySelectProps<CustomEntityForSelect>) => {
const containerRef = useRef<HTMLDivElement>(null);
const { searchFilter, handleSearchFilterChange } = useEntitySelectSearch();
const showCreateButton = isDefined(onCreate) && searchFilter !== '';
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
onCancel?.();
},
});
return (
<DropdownMenu
disableBlur={disableBackgroundBlur}
ref={containerRef}
width={width}
data-select-disable
>
<DropdownMenuSearchInput
value={searchFilter}
onChange={handleSearchFilterChange}
autoFocus
/>
<DropdownMenuSeparator />
<SingleEntitySelectBase
{...{
EmptyIcon,
emptyLabel,
entitiesToSelect,
loading,
onCancel,
onCreate,
onEntitySelected,
selectedEntity,
showCreateButton,
}}
/>
</DropdownMenu>
);
};

View File

@ -0,0 +1,169 @@
import { useRef } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { Key } from 'ts-key-enum';
import { IconPlus } from '@/ui/display/icon';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect';
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { Avatar } from '@/users/components/Avatar';
import { assertNotNull } from '~/utils/assert';
import { CreateNewButton } from '../../../ui/input/relation-picker/components/CreateNewButton';
import { DropdownMenuSkeletonItem } from '../../../ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem';
import { CreateButtonId, EmptyButtonId } from '../constants';
import { useEntitySelectScroll } from '../hooks/useEntitySelectScroll';
import { EntityForSelect } from '../types/EntityForSelect';
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
export type SingleEntitySelectBaseProps<
CustomEntityForSelect extends EntityForSelect,
> = {
EmptyIcon?: IconComponent;
emptyLabel?: string;
entitiesToSelect: CustomEntityForSelect[];
loading?: boolean;
onCancel?: () => void;
onEntitySelected: (entity?: CustomEntityForSelect) => void;
selectedEntity?: CustomEntityForSelect;
onCreate?: () => void;
showCreateButton?: boolean;
SelectAllIcon?: IconComponent;
selectAllLabel?: string;
isAllEntitySelected?: boolean;
isAllEntitySelectShown?: boolean;
onAllEntitySelected?: () => void;
};
export const SingleEntitySelectBase = <
CustomEntityForSelect extends EntityForSelect,
>({
EmptyIcon,
emptyLabel,
entitiesToSelect,
loading,
onCancel,
onEntitySelected,
selectedEntity,
onCreate,
showCreateButton,
SelectAllIcon,
selectAllLabel,
isAllEntitySelected,
isAllEntitySelectShown,
onAllEntitySelected,
}: SingleEntitySelectBaseProps<CustomEntityForSelect>) => {
const containerRef = useRef<HTMLDivElement>(null);
const entitiesInDropdown = [selectedEntity, ...entitiesToSelect].filter(
(entity): entity is CustomEntityForSelect =>
assertNotNull(entity) && isNonEmptyString(entity.name),
);
const { preselectedOptionId, resetScroll } = useEntitySelectScroll({
selectableOptionIds: [
EmptyButtonId,
...entitiesInDropdown.map((item) => item.id),
...(showCreateButton ? [CreateButtonId] : []),
],
containerRef,
});
useScopedHotkeys(
Key.Enter,
() => {
if (showCreateButton && preselectedOptionId === CreateButtonId) {
onCreate?.();
} else {
const entity = entitiesInDropdown.findIndex(
(entity) => entity.id === preselectedOptionId,
);
onEntitySelected(entitiesInDropdown[entity]);
}
resetScroll();
},
RelationPickerHotkeyScope.RelationPicker,
[entitiesInDropdown, preselectedOptionId, onEntitySelected],
);
useScopedHotkeys(
Key.Escape,
() => {
onCancel?.();
},
RelationPickerHotkeyScope.RelationPicker,
[onCancel],
);
return (
<div ref={containerRef}>
<DropdownMenuItemsContainer hasMaxHeight>
{loading ? (
<DropdownMenuSkeletonItem />
) : entitiesInDropdown.length === 0 && !isAllEntitySelectShown ? (
<MenuItem text="No result" />
) : (
<>
{isAllEntitySelectShown &&
selectAllLabel &&
onAllEntitySelected && (
<MenuItemSelect
onClick={() => onAllEntitySelected()}
LeftIcon={SelectAllIcon}
text={selectAllLabel}
hovered={preselectedOptionId === EmptyButtonId}
selected={!!isAllEntitySelected}
/>
)}
{emptyLabel && (
<MenuItemSelect
onClick={() => onEntitySelected()}
LeftIcon={EmptyIcon}
text={emptyLabel}
hovered={preselectedOptionId === EmptyButtonId}
selected={!selectedEntity}
/>
)}
{entitiesInDropdown?.map((entity) => (
<MenuItemSelectAvatar
key={entity.id}
testId="menu-item"
selected={selectedEntity?.id === entity.id}
onClick={() => onEntitySelected(entity)}
text={entity.name}
hovered={preselectedOptionId === entity.id}
avatar={
<Avatar
avatarUrl={entity.avatarUrl}
colorId={entity.id}
placeholder={entity.name}
size="md"
type={entity.avatarType ?? 'rounded'}
/>
}
/>
))}
</>
)}
</DropdownMenuItemsContainer>
{showCreateButton && (
<>
<DropdownMenuItemsContainer hasMaxHeight>
<DropdownMenuSeparator />
<CreateNewButton
onClick={onCreate}
LeftIcon={IconPlus}
text="Add New"
hovered={preselectedOptionId === CreateButtonId}
/>
</DropdownMenuItemsContainer>
</>
)}
</div>
);
};

View File

@ -0,0 +1,97 @@
import { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { IconUserCircle } from '@/ui/display/icon';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { mockedPeopleData } from '~/testing/mock-data/people';
import { sleep } from '~/testing/sleep';
import { EntityForSelect } from '../../types/EntityForSelect';
import { SingleEntitySelect } from '../SingleEntitySelect';
const entities = mockedPeopleData.map<EntityForSelect>((person) => ({
id: person.id,
name: person.name.firstName + ' ' + person.name.lastName,
record: person,
}));
const meta: Meta<typeof SingleEntitySelect> = {
title: 'UI/Input/RelationPicker/SingleEntitySelect',
component: SingleEntitySelect,
decorators: [ComponentDecorator, ComponentWithRecoilScopeDecorator],
argTypes: {
selectedEntity: {
options: entities.map(({ name }) => name),
mapping: entities.reduce(
(result, entity) => ({ ...result, [entity.name]: entity }),
{},
),
},
},
render: ({
EmptyIcon,
disableBackgroundBlur = false,
emptyLabel,
loading,
onCancel,
onCreate,
onEntitySelected,
selectedEntity,
width,
}) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { relationPickerSearchFilter } = useRelationPicker();
return (
<SingleEntitySelect
{...{
EmptyIcon,
disableBackgroundBlur,
emptyLabel,
loading,
onCancel,
onCreate,
onEntitySelected,
selectedEntity,
width,
}}
entitiesToSelect={entities.filter(
(entity) =>
entity.id !== selectedEntity?.id &&
entity.name.includes(relationPickerSearchFilter),
)}
/>
);
},
};
export default meta;
type Story = StoryObj<typeof SingleEntitySelect>;
export const Default: Story = {};
export const WithSelectedEntity: Story = {
args: { selectedEntity: entities[2] },
};
export const WithEmptyOption: Story = {
args: {
EmptyIcon: IconUserCircle,
emptyLabel: 'Nobody',
},
};
export const WithSearchFilter: Story = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
const searchInput = canvas.getByRole('textbox');
await step('Enter search text', async () => {
await sleep(50);
await userEvent.type(searchInput, 'a');
await expect(searchInput).toHaveValue('a');
});
},
};

View File

@ -0,0 +1,2 @@
export const CreateButtonId = 'create-button';
export const EmptyButtonId = 'empty-button';

View File

@ -0,0 +1,31 @@
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
import { getRelationPickerScopedStates } from '@/object-record/relation-picker/utils/getRelationPickerScopedStates';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
export const useRelationPickerScopedStates = (args?: {
relationPickerScopedId?: string;
}) => {
const { relationPickerScopedId } = args ?? {};
const scopeId = useAvailableScopeIdOrThrow(
RecordTableScopeInternalContext,
relationPickerScopedId,
);
const {
identifiersMapperState,
relationPickerSearchFilterState,
relationPickerPreselectedIdState,
searchQueryState,
} = getRelationPickerScopedStates({
relationPickerScopeId: scopeId,
});
return {
scopeId,
identifiersMapperState,
relationPickerSearchFilterState,
relationPickerPreselectedIdState,
searchQueryState,
};
};

View File

@ -0,0 +1,101 @@
import scrollIntoView from 'scroll-into-view';
import { Key } from 'ts-key-enum';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { CreateButtonId } from '../constants';
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
import { getPreselectedIdIndex } from '../utils/getPreselectedIdIndex';
export const useEntitySelectScroll = ({
containerRef,
selectableOptionIds,
}: {
selectableOptionIds: string[];
containerRef: React.RefObject<HTMLDivElement>;
}) => {
const { relationPickerPreselectedId, setRelationPickerPreselectedId } =
useRelationPicker();
const preselectedIdIndex = getPreselectedIdIndex(
selectableOptionIds,
relationPickerPreselectedId ?? '',
);
const resetScroll = () => {
setRelationPickerPreselectedId('');
const preselectedRef = containerRef.current?.children[0] as HTMLElement;
scrollIntoView(preselectedRef, {
align: {
top: 0,
},
isScrollable: (target) => {
return target === containerRef.current;
},
time: 0,
});
};
useScopedHotkeys(
Key.ArrowUp,
() => {
const previousSelectableIndex = Math.max(preselectedIdIndex - 1, 0);
const previousSelectableId = selectableOptionIds[previousSelectableIndex];
setRelationPickerPreselectedId(previousSelectableId);
const preselectedRef = containerRef.current?.children[
previousSelectableIndex
] as HTMLElement;
if (preselectedRef) {
scrollIntoView(preselectedRef, {
align: {
top: 0.5,
},
isScrollable: (target) => {
return target === containerRef.current;
},
time: 0,
});
}
},
RelationPickerHotkeyScope.RelationPicker,
[selectableOptionIds],
);
useScopedHotkeys(
Key.ArrowDown,
() => {
const nextSelectableIndex = Math.min(
preselectedIdIndex + 1,
selectableOptionIds?.length - 1,
);
const nextSelectableId = selectableOptionIds[nextSelectableIndex];
setRelationPickerPreselectedId(nextSelectableId);
if (nextSelectableId !== CreateButtonId) {
const preselectedRef = containerRef.current?.children[
nextSelectableIndex
] as HTMLElement;
if (preselectedRef) {
scrollIntoView(preselectedRef, {
align: {
top: 0.15,
},
isScrollable: (target) => target === containerRef.current,
time: 0,
});
}
}
},
RelationPickerHotkeyScope.RelationPicker,
[selectableOptionIds],
);
return {
preselectedOptionId: relationPickerPreselectedId,
resetScroll,
};
};

View File

@ -0,0 +1,31 @@
import debounce from 'lodash.debounce';
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
export const useEntitySelectSearch = () => {
const {
setRelationPickerPreselectedId,
relationPickerSearchFilter,
setRelationPickerSearchFilter,
} = useRelationPicker();
const debouncedSetSearchFilter = debounce(
setRelationPickerSearchFilter,
100,
{
leading: true,
},
);
const handleSearchFilterChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
debouncedSetSearchFilter(event.currentTarget.value);
setRelationPickerPreselectedId('');
};
return {
searchFilter: relationPickerSearchFilter,
handleSearchFilterChange,
};
};

View File

@ -0,0 +1,49 @@
import { useRecoilState } from 'recoil';
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
type useRelationPickeProps = {
relationPickerScopeId?: string;
};
export const useRelationPicker = (props?: useRelationPickeProps) => {
const scopeId = useAvailableScopeIdOrThrow(
RelationPickerScopeInternalContext,
props?.relationPickerScopeId,
);
const {
identifiersMapperState,
searchQueryState,
relationPickerSearchFilterState,
relationPickerPreselectedIdState,
} = useRelationPickerScopedStates({
relationPickerScopedId: scopeId,
});
const [identifiersMapper, setIdentifiersMapper] = useRecoilState(
identifiersMapperState,
);
const [searchQuery, setSearchQuery] = useRecoilState(searchQueryState);
const [relationPickerSearchFilter, setRelationPickerSearchFilter] =
useRecoilState(relationPickerSearchFilterState);
const [relationPickerPreselectedId, setRelationPickerPreselectedId] =
useRecoilState(relationPickerPreselectedIdState);
return {
scopeId,
identifiersMapper,
setIdentifiersMapper,
searchQuery,
setSearchQuery,
relationPickerSearchFilter,
setRelationPickerSearchFilter,
relationPickerPreselectedId,
setRelationPickerPreselectedId,
};
};

View File

@ -0,0 +1,21 @@
import { ReactNode } from 'react';
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
type RelationPickerScopeProps = {
children: ReactNode;
relationPickerScopeId: string;
};
export const RelationPickerScope = ({
children,
relationPickerScopeId,
}: RelationPickerScopeProps) => {
return (
<RelationPickerScopeInternalContext.Provider
value={{ scopeId: relationPickerScopeId }}
>
{children}
</RelationPickerScopeInternalContext.Provider>
);
};

View File

@ -0,0 +1,7 @@
import { ScopedStateKey } from '@/ui/utilities/recoil-scope/scopes-internal/types/ScopedStateKey';
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
type RelationPickerScopeInternalContextProps = ScopedStateKey;
export const RelationPickerScopeInternalContext =
createScopeInternalContext<RelationPickerScopeInternalContextProps>();

View File

@ -0,0 +1,8 @@
import { IdentifiersMapper } from '@/object-record/relation-picker/types/IdentifiersMapper';
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const identifiersMapperScopedState =
createScopedState<IdentifiersMapper | null>({
key: 'identifiersMapperScopedState',
defaultValue: null,
});

View File

@ -0,0 +1,5 @@
import { createContext } from 'react';
export const RelationPickerRecoilScopeContext = createContext<string | null>(
'relation-picker-context',
);

View File

@ -0,0 +1,8 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const relationPickerPreselectedIdScopedState = createScopedState<
string | undefined
>({
key: 'relationPickerPreselectedIdScopedState',
defaultValue: undefined,
});

View File

@ -0,0 +1,6 @@
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const relationPickerSearchFilterScopedState = createScopedState<string>({
key: 'relationPickerSearchFilterScopedState',
defaultValue: '',
});

View File

@ -0,0 +1,7 @@
import { SearchQuery } from '@/object-record/relation-picker/types/SearchQuery';
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const searchQueryScopedState = createScopedState<SearchQuery | null>({
key: 'searchQueryScopedState',
defaultValue: null,
});

View File

@ -0,0 +1,9 @@
import { AvatarType } from '@/users/components/Avatar';
export type EntityForSelect = {
id: string;
name: string;
avatarUrl?: string;
avatarType?: AvatarType;
record: any;
};

View File

@ -0,0 +1,14 @@
import { AvatarType } from '@/users/components/Avatar';
type RecordMappedToIdentifiers = {
id: string;
name: string;
avatarUrl?: string;
avatarType: AvatarType;
record: any;
};
export type IdentifiersMapper = (
record: any,
relationPickerType: string,
) => RecordMappedToIdentifiers | undefined;

View File

@ -0,0 +1,3 @@
export enum RelationPickerHotkeyScope {
RelationPicker = 'relation-picker',
}

View File

@ -0,0 +1,3 @@
export type SearchQuery = {
computeFilterFields: (relationPickerType: string) => string[];
};

View File

@ -0,0 +1,10 @@
export const getPreselectedIdIndex = (
selectableOptionIds: string[],
preselectedOptionId: string,
) => {
const preselectedIdIndex = selectableOptionIds.findIndex(
(option) => option === preselectedOptionId,
);
return preselectedIdIndex === -1 ? 0 : preselectedIdIndex;
};

View File

@ -0,0 +1,38 @@
import { identifiersMapperScopedState } from '@/object-record/relation-picker/states/identifiersMapperScopedState';
import { relationPickerPreselectedIdScopedState } from '@/object-record/relation-picker/states/relationPickerPreselectedIdScopedState';
import { relationPickerSearchFilterScopedState } from '@/object-record/relation-picker/states/relationPickerSearchFilterScopedState';
import { searchQueryScopedState } from '@/object-record/relation-picker/states/searchQueryScopedState';
import { getScopedState } from '@/ui/utilities/recoil-scope/utils/getScopedState';
export const getRelationPickerScopedStates = ({
relationPickerScopeId,
}: {
relationPickerScopeId: string;
}) => {
const identifiersMapperState = getScopedState(
identifiersMapperScopedState,
relationPickerScopeId,
);
const searchQueryState = getScopedState(
searchQueryScopedState,
relationPickerScopeId,
);
const relationPickerPreselectedIdState = getScopedState(
relationPickerPreselectedIdScopedState,
relationPickerScopeId,
);
const relationPickerSearchFilterState = getScopedState(
relationPickerSearchFilterScopedState,
relationPickerScopeId,
);
return {
identifiersMapperState,
relationPickerSearchFilterState,
relationPickerPreselectedIdState,
searchQueryState,
};
};