Chore/move records related to record folder (#2859)

* WIP

* Finished multi select filter

* Cleaned console log

* Fix naming

* Fixed naming

* Moved RelationPicker folder

* Moved EntitySelect components

* Moved story

* Moved RelationPicker non component folders

* Moved everything else
This commit is contained in:
Lucas Bordeau
2023-12-07 12:43:10 +01:00
committed by GitHub
parent ef536ebb06
commit a8ecc23cbe
445 changed files with 407 additions and 412 deletions
@@ -1,7 +1,7 @@
import { ChangeEvent } from 'react';
import styled from '@emotion/styled';
import { StyledInput } from '@/ui/object/field/meta-types/input/components/internal/TextInput';
import { StyledInput } from '@/object-record/field/meta-types/input/components/internal/TextInput';
import { ComputeNodeDimensions } from '@/ui/utilities/dimensions/components/ComputeNodeDimensions';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
@@ -1,97 +0,0 @@
import { useEffect } from 'react';
import { useQuery } from '@apollo/client';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { IconForbid } from '@/ui/display/icon';
import { useRelationPicker } from '@/ui/input/components/internal/relation-picker/hooks/useRelationPicker';
import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect';
import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { FieldDefinition } from '@/ui/object/field/types/FieldDefinition';
import { FieldRelationMetadata } from '@/ui/object/field/types/FieldMetadata';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
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] =
useRecoilScopedState(relationPickerSearchFilterScopedState);
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}
/>
);
};
@@ -1,28 +0,0 @@
import { getRelationPickerScopedStates } from '@/ui/input/components/internal/relation-picker/utils/getRelationPickerScopedStates';
import { RecordTableScopeInternalContext } from '@/ui/object/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
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 } = getRelationPickerScopedStates({
relationPickerScopeId: scopeId,
});
const { searchQueryState } = getRelationPickerScopedStates({
relationPickerScopeId: scopeId,
});
return {
scopeId,
identifiersMapperState,
searchQueryState,
};
};
@@ -1,35 +0,0 @@
import { useRecoilState } from 'recoil';
import { useRelationPickerScopedStates } from '@/ui/input/components/internal/relation-picker/hooks/internal/useRelationPickerScopedStates';
import { RelationPickerScopeInternalContext } from '@/ui/input/components/internal/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 } =
useRelationPickerScopedStates({
relationPickerScopedId: scopeId,
});
const [identifiersMapper, setIdentifiersMapper] = useRecoilState(
identifiersMapperState,
);
const [searchQuery, setSearchQuery] = useRecoilState(searchQueryState);
return {
scopeId,
identifiersMapper,
setIdentifiersMapper,
searchQuery,
setSearchQuery,
};
};
@@ -1,21 +0,0 @@
import { ReactNode } from 'react';
import { RelationPickerScopeInternalContext } from '@/ui/input/components/internal/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>
);
};
@@ -1,7 +0,0 @@
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>();
@@ -1,8 +0,0 @@
import { IdentifiersMapper } from '@/ui/input/components/internal/relation-picker/types/IdentifiersMapper';
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const identifiersMapperScopedState =
createScopedState<IdentifiersMapper | null>({
key: 'identifiersMapperScopedState',
defaultValue: null,
});
@@ -1,7 +0,0 @@
import { SearchQuery } from '@/ui/input/components/internal/relation-picker/types/SearchQuery';
import { createScopedState } from '@/ui/utilities/recoil-scope/utils/createScopedState';
export const searchQueryScopedState = createScopedState<SearchQuery | null>({
key: 'searchQueryScopedState',
defaultValue: null,
});
@@ -1,14 +0,0 @@
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;
@@ -1,3 +0,0 @@
export type SearchQuery = {
computeFilterFields: (relationPickerType: string) => string[];
};
@@ -1,24 +0,0 @@
import { identifiersMapperScopedState } from '@/ui/input/components/internal/relation-picker/states/identifiersMapperScopedState';
import { searchQueryScopedState } from '@/ui/input/components/internal/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,
);
return {
identifiersMapperState,
searchQueryState,
};
};
@@ -1,106 +0,0 @@
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>
);
};
@@ -1,91 +0,0 @@
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>
);
};
@@ -1,170 +0,0 @@
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 { CreateButtonId, EmptyButtonId } from '../constants';
import { useEntitySelectScroll } from '../hooks/useEntitySelectScroll';
import { EntityForSelect } from '../types/EntityForSelect';
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
import { DropdownMenuSkeletonItem } from './skeletons/DropdownMenuSkeletonItem';
import { CreateNewButton } from './CreateNewButton';
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>
);
};
@@ -1,101 +0,0 @@
import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconUserCircle } from '@/ui/display/icon';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
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 { relationPickerSearchFilterScopedState } from '../../states/relationPickerSearchFilterScopedState';
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 = useRecoilScopedValue(
relationPickerSearchFilterScopedState,
);
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');
});
},
};
@@ -1,2 +0,0 @@
export const CreateButtonId = 'create-button';
export const EmptyButtonId = 'empty-button';
@@ -1,106 +0,0 @@
import scrollIntoView from 'scroll-into-view';
import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { CreateButtonId } from '../constants';
import { RelationPickerRecoilScopeContext } from '../states/recoil-scope-contexts/RelationPickerRecoilScopeContext';
import { relationPickerPreselectedIdScopedState } from '../states/relationPickerPreselectedIdScopedState';
import { RelationPickerHotkeyScope } from '../types/RelationPickerHotkeyScope';
import { getPreselectedIdIndex } from '../utils/getPreselectedIdIndex';
export const useEntitySelectScroll = ({
containerRef,
selectableOptionIds,
}: {
selectableOptionIds: string[];
containerRef: React.RefObject<HTMLDivElement>;
}) => {
const [relationPickerPreselectedId, setRelationPickerPreselectedId] =
useRecoilScopedState(
relationPickerPreselectedIdScopedState,
RelationPickerRecoilScopeContext,
);
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,
};
};
@@ -1,40 +0,0 @@
import debounce from 'lodash.debounce';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { RelationPickerRecoilScopeContext } from '../states/recoil-scope-contexts/RelationPickerRecoilScopeContext';
import { relationPickerPreselectedIdScopedState } from '../states/relationPickerPreselectedIdScopedState';
import { relationPickerSearchFilterScopedState } from '../states/relationPickerSearchFilterScopedState';
export const useEntitySelectSearch = () => {
const [, setRelationPickerPreselectedId] = useRecoilScopedState(
relationPickerPreselectedIdScopedState,
RelationPickerRecoilScopeContext,
);
const [relationPickerSearchFilter, setRelationPickerSearchFilter] =
useRecoilScopedState(
relationPickerSearchFilterScopedState,
RelationPickerRecoilScopeContext,
);
const debouncedSetSearchFilter = debounce(
setRelationPickerSearchFilter,
100,
{
leading: true,
},
);
const handleSearchFilterChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
debouncedSetSearchFilter(event.currentTarget.value);
setRelationPickerPreselectedId('');
};
return {
searchFilter: relationPickerSearchFilter,
handleSearchFilterChange,
};
};
@@ -1,5 +0,0 @@
import { createContext } from 'react';
export const RelationPickerRecoilScopeContext = createContext<string | null>(
'relation-picker-context',
);
@@ -1,9 +0,0 @@
import { atomFamily } from 'recoil';
export const relationPickerPreselectedIdScopedState = atomFamily<
string,
string
>({
key: 'relationPickerPreselectedIdScopedState',
default: (param) => param,
});
@@ -1,8 +0,0 @@
import { atomFamily } from 'recoil';
export const relationPickerSearchFilterScopedState = atomFamily<string, string>(
{
key: 'relationPickerSearchFilterScopedState',
default: '',
},
);
@@ -1,9 +0,0 @@
import { AvatarType } from '@/users/components/Avatar';
export type EntityForSelect = {
id: string;
name: string;
avatarUrl?: string;
avatarType?: AvatarType;
record: any;
};
@@ -1,3 +0,0 @@
export enum RelationPickerHotkeyScope {
RelationPicker = 'relation-picker',
}
@@ -1,10 +0,0 @@
export const getPreselectedIdIndex = (
selectableOptionIds: string[],
preselectedOptionId: string,
) => {
const preselectedIdIndex = selectableOptionIds.findIndex(
(option) => option === preselectedOptionId,
);
return preselectedIdIndex === -1 ? 0 : preselectedIdIndex;
};
@@ -1,4 +1,4 @@
import { TableHotkeyScope } from '@/ui/object/record-table/types/TableHotkeyScope';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
type PageHotkeysEffectProps = {
@@ -2,7 +2,7 @@ import { MemoryRouter } from 'react-router-dom';
import { Meta, StoryObj } from '@storybook/react';
import { useSetRecoilState } from 'recoil';
import { RecordTableScope } from '@/ui/object/record-table/scopes/RecordTableScope';
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { actionBarOpenState } from '../../states/actionBarIsOpenState';
@@ -2,7 +2,7 @@ import { MemoryRouter } from 'react-router-dom';
import { Meta, StoryObj } from '@storybook/react';
import { useSetRecoilState } from 'recoil';
import { RecordTableScope } from '@/ui/object/record-table/scopes/RecordTableScope';
import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { contextMenuIsOpenState } from '../../states/contextMenuIsOpenState';
@@ -1,66 +0,0 @@
import { useContext } from 'react';
import { FieldContext } from '../contexts/FieldContext';
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
import { CurrencyFieldDisplay } from '../meta-types/display/components/CurrencyFieldDisplay';
import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay';
import { FullNameFieldDisplay } from '../meta-types/display/components/FullNameFieldDisplay';
import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisplay';
import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay';
import { RelationFieldDisplay } from '../meta-types/display/components/RelationFieldDisplay';
import { SelectFieldDisplay } from '../meta-types/display/components/SelectFieldDisplay';
import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay';
import { UuidFieldDisplay } from '../meta-types/display/components/UuidFieldDisplay';
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
import { isFieldDateTime } from '../types/guards/isFieldDateTime';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldFullName } from '../types/guards/isFieldFullName';
import { isFieldLink } from '../types/guards/isFieldLink';
import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldSelect } from '../types/guards/isFieldSelect';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldUuid } from '../types/guards/isFieldUuid';
export const FieldDisplay = () => {
const { fieldDefinition, isLabelIdentifier } = useContext(FieldContext);
if (
isLabelIdentifier &&
(isFieldText(fieldDefinition) || isFieldFullName(fieldDefinition))
) {
return <ChipFieldDisplay />;
}
return (
<>
{isFieldRelation(fieldDefinition) ? (
<RelationFieldDisplay />
) : isFieldText(fieldDefinition) ? (
<TextFieldDisplay />
) : isFieldUuid(fieldDefinition) ? (
<UuidFieldDisplay />
) : isFieldEmail(fieldDefinition) ? (
<EmailFieldDisplay />
) : isFieldDateTime(fieldDefinition) ? (
<DateFieldDisplay />
) : isFieldNumber(fieldDefinition) ? (
<NumberFieldDisplay />
) : isFieldLink(fieldDefinition) ? (
<LinkFieldDisplay />
) : isFieldCurrency(fieldDefinition) ? (
<CurrencyFieldDisplay />
) : isFieldFullName(fieldDefinition) ? (
<FullNameFieldDisplay />
) : isFieldPhone(fieldDefinition) ? (
<PhoneFieldDisplay />
) : isFieldSelect(fieldDefinition) ? (
<SelectFieldDisplay />
) : (
<></>
)}
</>
);
};
@@ -1,130 +0,0 @@
import { useContext } from 'react';
import { FullNameFieldInput } from '@/ui/object/field/meta-types/input/components/FullNameFieldInput';
import { isFieldFullName } from '@/ui/object/field/types/guards/isFieldFullName';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { FieldContext } from '../contexts/FieldContext';
import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput';
import { CurrencyFieldInput } from '../meta-types/input/components/CurrencyFieldInput';
import { DateFieldInput } from '../meta-types/input/components/DateFieldInput';
import { EmailFieldInput } from '../meta-types/input/components/EmailFieldInput';
import { LinkFieldInput } from '../meta-types/input/components/LinkFieldInput';
import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInput';
import { PhoneFieldInput } from '../meta-types/input/components/PhoneFieldInput';
import { RatingFieldInput } from '../meta-types/input/components/RatingFieldInput';
import { RelationFieldInput } from '../meta-types/input/components/RelationFieldInput';
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
import { FieldInputEvent } from '../types/FieldInputEvent';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
import { isFieldDateTime } from '../types/guards/isFieldDateTime';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldLink } from '../types/guards/isFieldLink';
import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldRating } from '../types/guards/isFieldRating';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldText } from '../types/guards/isFieldText';
type FieldInputProps = {
onSubmit?: FieldInputEvent;
onCancel?: () => void;
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const FieldInput = ({
onCancel,
onSubmit,
onEnter,
onEscape,
onShiftTab,
onTab,
onClickOutside,
}: FieldInputProps) => {
const { fieldDefinition } = useContext(FieldContext);
return (
<>
{isFieldRelation(fieldDefinition) ? (
<RecoilScope>
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
</RecoilScope>
) : isFieldPhone(fieldDefinition) ? (
<PhoneFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldText(fieldDefinition) ? (
<TextFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldEmail(fieldDefinition) ? (
<EmailFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldFullName(fieldDefinition) ? (
<FullNameFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldDateTime(fieldDefinition) ? (
<DateFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldNumber(fieldDefinition) ? (
<NumberFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldLink(fieldDefinition) ? (
<LinkFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldCurrency(fieldDefinition) ? (
<CurrencyFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldBoolean(fieldDefinition) ? (
<BooleanFieldInput onSubmit={onSubmit} />
) : isFieldRating(fieldDefinition) ? (
<RatingFieldInput onSubmit={onSubmit} />
) : (
<></>
)}
</>
);
};
@@ -1,19 +0,0 @@
import { createContext } from 'react';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldMetadata } from '../types/FieldMetadata';
export type GenericFieldContextType = {
fieldDefinition: FieldDefinition<FieldMetadata>;
// TODO: add better typing for mutation web-hook
useUpdateEntityMutation?: () => [(params: any) => void, any];
entityId: string;
recoilScopeId?: string;
hotkeyScope: string;
isLabelIdentifier: boolean;
basePathToShowPage?: string;
};
export const FieldContext = createContext<GenericFieldContextType>(
{} as GenericFieldContextType,
);
@@ -1,18 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { FieldContext } from '../contexts/FieldContext';
import { entityFieldInitialValueFamilyState } from '../states/entityFieldInitialValueFamilyState';
export const useFieldInitialValue = () => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const fieldInitialValue = useRecoilValue(
entityFieldInitialValueFamilyState({
fieldMetadataId: fieldDefinition.fieldMetadataId,
entityId,
}),
);
return fieldInitialValue;
};
@@ -1,33 +0,0 @@
import { useContext } from 'react';
import { IconPencil } from '@/ui/display/icon';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { isFieldRelation } from '@/ui/object/field/types/guards/isFieldRelation';
import { FieldContext } from '../contexts/FieldContext';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldLink } from '../types/guards/isFieldLink';
import { isFieldPhone } from '../types/guards/isFieldPhone';
export const useGetButtonIcon = (): IconComponent | undefined => {
const { fieldDefinition } = useContext(FieldContext);
if (!fieldDefinition) return undefined;
if (
isFieldLink(fieldDefinition) ||
isFieldEmail(fieldDefinition) ||
isFieldPhone(fieldDefinition)
) {
return IconPencil;
}
if (isFieldRelation(fieldDefinition)) {
if (
fieldDefinition.metadata.relationObjectMetadataNameSingular !==
'workspaceMember'
) {
return IconPencil;
}
}
};
@@ -1,21 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { FieldContext } from '../contexts/FieldContext';
import { isEntityFieldEmptyFamilySelector } from '../states/selectors/isEntityFieldEmptyFamilySelector';
export const useIsFieldEmpty = () => {
const { entityId, fieldDefinition } = useContext(FieldContext);
const isFieldEmpty = useRecoilValue(
isEntityFieldEmptyFamilySelector({
fieldDefinition: {
type: fieldDefinition.type,
},
fieldName: fieldDefinition.metadata.fieldName,
entityId,
}),
);
return isFieldEmpty;
};
@@ -1,15 +0,0 @@
import { useContext } from 'react';
import { FieldContext } from '../contexts/FieldContext';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldRating } from '../types/guards/isFieldRating';
export const useIsFieldInputOnly = () => {
const { fieldDefinition } = useContext(FieldContext);
if (isFieldBoolean(fieldDefinition) || isFieldRating(fieldDefinition)) {
return true;
}
return false;
};
@@ -1,138 +0,0 @@
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { isFieldFullName } from '@/ui/object/field/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/ui/object/field/types/guards/isFieldFullNameValue';
import { FieldContext } from '../contexts/FieldContext';
import { entityFieldsFamilySelector } from '../states/selectors/entityFieldsFamilySelector';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
import { isFieldCurrencyValue } from '../types/guards/isFieldCurrencyValue';
import { isFieldDateTime } from '../types/guards/isFieldDateTime';
import { isFieldDateTimeValue } from '../types/guards/isFieldDateTimeValue';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldEmailValue } from '../types/guards/isFieldEmailValue';
import { isFieldLink } from '../types/guards/isFieldLink';
import { isFieldLinkValue } from '../types/guards/isFieldLinkValue';
import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldNumberValue } from '../types/guards/isFieldNumberValue';
import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldPhoneValue } from '../types/guards/isFieldPhoneValue';
import { isFieldRating } from '../types/guards/isFieldRating';
import { isFieldRatingValue } from '../types/guards/isFieldRatingValue';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldRelationValue } from '../types/guards/isFieldRelationValue';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldTextValue } from '../types/guards/isFieldTextValue';
export const usePersistField = () => {
const {
entityId,
fieldDefinition,
useUpdateEntityMutation = () => [],
} = useContext(FieldContext);
const [updateEntity] = useUpdateEntityMutation();
const persistField = useRecoilCallback(
({ set }) =>
(valueToPersist: unknown) => {
const fieldIsRelation =
isFieldRelation(fieldDefinition) &&
isFieldRelationValue(valueToPersist);
const fieldIsText =
isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist);
const fieldIsEmail =
isFieldEmail(fieldDefinition) && isFieldEmailValue(valueToPersist);
const fieldIsDateTime =
isFieldDateTime(fieldDefinition) &&
isFieldDateTimeValue(valueToPersist);
const fieldIsLink =
isFieldLink(fieldDefinition) && isFieldLinkValue(valueToPersist);
const fieldIsBoolean =
isFieldBoolean(fieldDefinition) &&
isFieldBooleanValue(valueToPersist);
const fieldIsProbability =
isFieldRating(fieldDefinition) && isFieldRatingValue(valueToPersist);
const fieldIsNumber =
isFieldNumber(fieldDefinition) && isFieldNumberValue(valueToPersist);
const fieldIsCurrency =
isFieldCurrency(fieldDefinition) &&
isFieldCurrencyValue(valueToPersist);
const fieldIsFullName =
isFieldFullName(fieldDefinition) &&
isFieldFullNameValue(valueToPersist);
const fieldIsPhone =
isFieldPhone(fieldDefinition) && isFieldPhoneValue(valueToPersist);
if (fieldIsRelation) {
const fieldName = fieldDefinition.metadata.fieldName;
set(
entityFieldsFamilySelector({ entityId, fieldName }),
valueToPersist,
);
updateEntity?.({
variables: {
where: { id: entityId },
data: {
// TODO: find a more elegant way to do this ?
// Maybe have a link between the RELATION field and the UUID field ?
[`${fieldName}Id`]: valueToPersist?.id ?? null,
},
},
});
} else if (
fieldIsText ||
fieldIsBoolean ||
fieldIsEmail ||
fieldIsProbability ||
fieldIsNumber ||
fieldIsDateTime ||
fieldIsPhone ||
fieldIsLink ||
fieldIsCurrency ||
fieldIsFullName
) {
const fieldName = fieldDefinition.metadata.fieldName;
set(
entityFieldsFamilySelector({ entityId, fieldName }),
valueToPersist,
);
updateEntity?.({
variables: {
where: { id: entityId },
data: {
[fieldName]: valueToPersist,
},
},
});
} else {
throw new Error(
`Invalid value to persist: ${JSON.stringify(
valueToPersist,
)} for type : ${
fieldDefinition.type
}, type may not be implemented in usePersistField.`,
);
}
},
[entityId, fieldDefinition, updateEntity],
);
return persistField;
};
@@ -1,51 +0,0 @@
import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { FieldContext } from '../contexts/FieldContext';
import { entityFieldsFamilySelector } from '../states/selectors/entityFieldsFamilySelector';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
export const useToggleEditOnlyInput = () => {
const {
entityId,
fieldDefinition,
useUpdateEntityMutation = () => [],
} = useContext(FieldContext);
const [updateEntity] = useUpdateEntityMutation();
const toggleField = useRecoilCallback(
({ set, snapshot }) =>
() => {
const fieldIsBoolean = isFieldBoolean(fieldDefinition);
if (fieldIsBoolean) {
const fieldName = fieldDefinition.metadata.fieldName;
const oldValue = snapshot
.getLoadable(entityFieldsFamilySelector({ entityId, fieldName }))
.valueOrThrow();
const valueToPersist = !oldValue;
set(
entityFieldsFamilySelector({ entityId, fieldName }),
valueToPersist,
);
updateEntity?.({
variables: {
where: { id: entityId },
data: {
[fieldName]: valueToPersist,
},
},
});
} else {
throw new Error(
`Invalid value to toggle for type : ${fieldDefinition}, type may not be implemented in useToggleEditOnlyInput.`,
);
}
},
[entityId, fieldDefinition, updateEntity],
);
return toggleField;
};
@@ -1,31 +0,0 @@
import {
FieldContext,
GenericFieldContextType,
} from '@/ui/object/field/contexts/FieldContext';
type FieldContextProviderProps = {
children: React.ReactNode;
fieldDefinition: GenericFieldContextType['fieldDefinition'];
entityId?: string;
};
export const FieldContextProvider = ({
children,
fieldDefinition,
entityId,
}: FieldContextProviderProps) => {
return (
<FieldContext.Provider
value={{
entityId: entityId ?? '1',
isLabelIdentifier: false,
recoilScopeId: '1',
hotkeyScope: 'hotkey-scope',
fieldDefinition,
useUpdateEntityMutation: () => [() => undefined, {}],
}}
>
{children}
</FieldContext.Provider>
);
};
@@ -1,24 +0,0 @@
import { EntityChip } from '@/ui/display/chip/components/EntityChip';
import { useChipField } from '@/ui/object/field/meta-types/hooks/useChipField';
export const ChipFieldDisplay = () => {
const {
record,
entityId,
identifiersMapper,
objectNameSingular,
basePathToShowPage,
} = useChipField();
const identifiers = identifiersMapper?.(record, objectNameSingular ?? '');
return (
<EntityChip
name={identifiers?.name ?? ''}
avatarUrl={identifiers?.avatarUrl}
avatarType={identifiers?.avatarType}
entityId={entityId}
linkToEntity={basePathToShowPage + entityId}
/>
);
};
@@ -1,8 +0,0 @@
import { useCurrencyField } from '../../hooks/useCurrencyField';
import { CurrencyDisplay } from '../content-display/components/CurrencyDisplay';
export const CurrencyFieldDisplay = () => {
const { initialAmount } = useCurrencyField();
return <CurrencyDisplay amount={initialAmount} />;
};
@@ -1,9 +0,0 @@
import { DateDisplay } from '@/ui/object/field/meta-types/display/content-display/components/DateDisplay';
import { useDateTimeField } from '../../hooks/useDateTimeField';
export const DateFieldDisplay = () => {
const { fieldValue } = useDateTimeField();
return <DateDisplay value={fieldValue} />;
};
@@ -1,8 +0,0 @@
import { useEmailField } from '../../hooks/useEmailField';
import { EmailDisplay } from '../content-display/components/EmailDisplay';
export const EmailFieldDisplay = () => {
const { fieldValue } = useEmailField();
return <EmailDisplay value={fieldValue} />;
};
@@ -1,13 +0,0 @@
import { useFullNameField } from '@/ui/object/field/meta-types/hooks/useFullNameField';
import { TextDisplay } from '../content-display/components/TextDisplay';
export const FullNameFieldDisplay = () => {
const { fieldValue } = useFullNameField();
const content = [fieldValue.firstName, fieldValue.lastName]
.filter(Boolean)
.join(' ');
return <TextDisplay text={content} />;
};
@@ -1,8 +0,0 @@
import { useLinkField } from '../../hooks/useLinkField';
import { LinkDisplay } from '../content-display/components/LinkDisplay';
export const LinkFieldDisplay = () => {
const { fieldValue } = useLinkField();
return <LinkDisplay value={fieldValue} />;
};
@@ -1,9 +0,0 @@
import { NumberDisplay } from '@/ui/object/field/meta-types/display/content-display/components/NumberDisplay';
import { useNumberField } from '../../hooks/useNumberField';
export const NumberFieldDisplay = () => {
const { fieldValue } = useNumberField();
return <NumberDisplay value={fieldValue} />;
};
@@ -1,9 +0,0 @@
import { PhoneDisplay } from '@/ui/object/field/meta-types/display/content-display/components/PhoneDisplay';
import { usePhoneField } from '../../hooks/usePhoneField';
export const PhoneFieldDisplay = () => {
const { fieldValue } = usePhoneField();
return <PhoneDisplay value={fieldValue} />;
};
@@ -1,28 +0,0 @@
import { EntityChip } from '@/ui/display/chip/components/EntityChip';
import { useRelationPicker } from '@/ui/input/components/internal/relation-picker/hooks/useRelationPicker';
import { useRelationField } from '../../hooks/useRelationField';
export const RelationFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useRelationField();
const { identifiersMapper } = useRelationPicker();
if (!fieldValue || !fieldDefinition || !identifiersMapper) {
return <></>;
}
const objectIdentifiers = identifiersMapper(
fieldValue,
fieldDefinition.metadata.relationObjectMetadataNameSingular,
);
return (
<EntityChip
entityId={fieldValue.id}
name={objectIdentifiers?.name ?? ''}
avatarUrl={objectIdentifiers?.avatarUrl}
avatarType={objectIdentifiers?.avatarType}
/>
);
};
@@ -1,9 +0,0 @@
import { Tag } from '@/ui/display/tag/components/Tag';
import { useSelectField } from '../../hooks/useSelectField';
export const SelectFieldDisplay = () => {
const { fieldValue } = useSelectField();
return <Tag color={fieldValue.color} text={fieldValue.label} />;
};
@@ -1,9 +0,0 @@
import { TextDisplay } from '@/ui/object/field/meta-types/display/content-display/components/TextDisplay';
import { useTextField } from '../../hooks/useTextField';
export const TextFieldDisplay = () => {
const { fieldValue } = useTextField();
return <TextDisplay text={fieldValue} />;
};
@@ -1,8 +0,0 @@
import { TextDisplay } from '@/ui/object/field/meta-types/display/content-display/components/TextDisplay';
import { useUuidField } from '@/ui/object/field/meta-types/hooks/useUuidField';
export const UuidFieldDisplay = () => {
const { fieldValue } = useUuidField();
return <TextDisplay text={fieldValue} />;
};
@@ -1,65 +0,0 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContext } from '../../../../contexts/FieldContext';
import { useDateTimeField } from '../../../hooks/useDateTimeField';
import { DateFieldDisplay } from '../DateFieldDisplay';
const formattedDate = new Date('2023-04-01');
const DateFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useDateTimeField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/DateFieldDisplay',
decorators: [
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'date',
label: 'Date',
type: 'DATE_TIME',
iconName: 'IconCalendarEvent',
metadata: {
fieldName: 'Date',
},
},
hotkeyScope: 'hotkey-scope',
}}
>
<DateFieldValueSetterEffect value={args.value} />
<Story />
</FieldContext.Provider>
),
ComponentDecorator,
],
component: DateFieldDisplay,
argTypes: { value: { control: 'date' } },
args: {
value: formattedDate,
},
};
export default meta;
type Story = StoryObj<typeof DateFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
};
@@ -1,66 +0,0 @@
import { useEffect } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContext } from '../../../../contexts/FieldContext';
import { useEmailField } from '../../../hooks/useEmailField';
import { EmailFieldDisplay } from '../EmailFieldDisplay';
const EmailFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useEmailField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/EmailFieldDisplay',
decorators: [
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'email',
label: 'Email',
type: 'EMAIL',
iconName: 'IconLink',
metadata: {
fieldName: 'Email',
placeHolder: 'Email',
},
},
hotkeyScope: 'hotkey-scope',
}}
>
<MemoryRouter>
<EmailFieldValueSetterEffect value={args.value} />
<Story />
</MemoryRouter>
</FieldContext.Provider>
),
ComponentDecorator,
],
component: EmailFieldDisplay,
args: {
value: 'Test@Test.test',
},
};
export default meta;
type Story = StoryObj<typeof EmailFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
};
@@ -1,81 +0,0 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContext } from '../../../../contexts/FieldContext';
import { useNumberField } from '../../../hooks/useNumberField';
import { NumberFieldDisplay } from '../NumberFieldDisplay';
const NumberFieldValueSetterEffect = ({ value }: { value: number }) => {
const { setFieldValue } = useNumberField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/NumberFieldDisplay',
decorators: [
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'number',
label: 'Number',
type: 'NUMBER',
iconName: 'Icon123',
metadata: {
fieldName: 'Number',
placeHolder: 'Number',
isPositive: true,
},
},
hotkeyScope: 'hotkey-scope',
useUpdateEntityMutation: () => [() => undefined, undefined],
}}
>
<NumberFieldValueSetterEffect value={args.value} />
<Story />
</FieldContext.Provider>
),
ComponentDecorator,
],
component: NumberFieldDisplay,
};
export default meta;
type Story = StoryObj<typeof NumberFieldDisplay>;
export const Default: Story = {
args: {
value: 100,
},
};
export const Elipsis: Story = {
args: {
value: 1e100,
},
parameters: {
container: { width: 100 },
},
};
export const Negative: Story = {
args: {
value: -1000,
},
};
export const Float: Story = {
args: {
value: 1.357802,
},
};
@@ -1,68 +0,0 @@
import { useEffect } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContext } from '../../../../contexts/FieldContext';
import { usePhoneField } from '../../../hooks/usePhoneField';
import { PhoneFieldDisplay } from '../PhoneFieldDisplay';
const PhoneFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = usePhoneField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/PhoneFieldDisplay',
decorators: [
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'phone',
label: 'Phone',
type: 'TEXT',
iconName: 'IconPhone',
metadata: {
fieldName: 'phone',
placeHolder: 'Phone',
objectMetadataNameSingular: 'person',
},
},
hotkeyScope: 'hotkey-scope',
useUpdateEntityMutation: () => [() => undefined, undefined],
}}
>
<MemoryRouter>
<PhoneFieldValueSetterEffect value={args.value} />
<Story />
</MemoryRouter>
</FieldContext.Provider>
),
ComponentDecorator,
],
component: PhoneFieldDisplay,
args: {
value: '362763872687362',
},
};
export default meta;
type Story = StoryObj<typeof PhoneFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
parameters: {
container: { width: 50 },
},
};
@@ -1,67 +0,0 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContext } from '../../../../contexts/FieldContext';
import { useTextField } from '../../../hooks/useTextField';
import { TextFieldDisplay } from '../TextFieldDisplay';
const TextFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useTextField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return null;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/TextFieldDisplay',
decorators: [
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
isLabelIdentifier: false,
fieldDefinition: {
fieldMetadataId: 'text',
label: 'Text',
type: 'TEXT',
iconName: 'IconLink',
metadata: {
fieldName: 'Text',
placeHolder: 'Text',
},
},
hotkeyScope: 'hotkey-scope',
}}
>
<TextFieldValueSetterEffect value={args.value} />
<Story />
</FieldContext.Provider>
),
ComponentDecorator,
],
component: TextFieldDisplay,
args: {
value: 'Lorem ipsum',
},
};
export default meta;
type Story = StoryObj<typeof TextFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
args: {
value:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae rerum fugiat veniam illum accusantium saepe, voluptate inventore libero doloribus doloremque distinctio blanditiis amet quis dolor a nulla? Placeat nam itaque rerum esse quidem animi, temporibus saepe debitis commodi quia eius eos minus inventore. Voluptates fugit optio sit ab consectetur ipsum, neque eius atque blanditiis. Ullam provident at porro minima, nobis vero dicta consequatur maxime laboriosam fugit repudiandae repellat tempore voluptas non voluptatibus neque aliquam ducimus doloribus ipsa? Sapiente suscipit unde modi commodi possimus doloribus eum voluptatibus, architecto laudantium, magnam, eos numquam exercitationem est maxime explicabo odio nemo qui distinctio temporibus.',
},
parameters: {
container: { width: 100 },
},
};
@@ -1,10 +0,0 @@
import { EllipsisDisplay } from './EllipsisDisplay';
type CurrencyDisplayProps = {
amount?: number | null;
};
// TODO: convert currencyCode to currency symbol
export const CurrencyDisplay = ({ amount }: CurrencyDisplayProps) => {
return <EllipsisDisplay>{amount}</EllipsisDisplay>;
};
@@ -1,11 +0,0 @@
import { formatToHumanReadableDate } from '~/utils';
import { EllipsisDisplay } from './EllipsisDisplay';
type DateDisplayProps = {
value: Date | string | null | undefined;
};
export const DateDisplay = ({ value }: DateDisplayProps) => (
<EllipsisDisplay>{value && formatToHumanReadableDate(value)}</EllipsisDisplay>
);
@@ -1,3 +0,0 @@
import { TextDisplay } from './TextDisplay';
export const DoubleTextDisplay = TextDisplay;
@@ -1,10 +0,0 @@
import styled from '@emotion/styled';
const StyledEllipsisDisplay = styled.div`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
`;
export { StyledEllipsisDisplay as EllipsisDisplay };
@@ -1,31 +0,0 @@
import { MouseEvent } from 'react';
import { ContactLink } from '@/ui/navigation/link/components/ContactLink';
import { EllipsisDisplay } from './EllipsisDisplay';
const validateEmail = (email: string) => {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test(email.trim());
};
type EmailDisplayProps = {
value: string | null;
};
export const EmailDisplay = ({ value }: EmailDisplayProps) => (
<EllipsisDisplay>
{value && validateEmail(value) ? (
<ContactLink
href={`mailto:${value}`}
onClick={(event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
}}
>
{value}
</ContactLink>
) : (
<ContactLink href="#">{value}</ContactLink>
)}
</EllipsisDisplay>
);
@@ -1,73 +0,0 @@
import { MouseEvent } from 'react';
import styled from '@emotion/styled';
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
import {
LinkType,
SocialLink,
} from '@/ui/navigation/link/components/SocialLink';
import { FieldLinkValue } from '@/ui/object/field/types/FieldMetadata';
import { EllipsisDisplay } from './EllipsisDisplay';
const StyledRawLink = styled(RoundedLink)`
overflow: hidden;
a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
type LinkDisplayProps = {
value?: FieldLinkValue;
};
const checkUrlType = (url: string) => {
if (
/^(http|https):\/\/(?:www\.)?linkedin.com(\w+:{0,1}\w*@)?(\S+)(:([0-9])+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(
url,
)
) {
return LinkType.LinkedIn;
}
if (url.match(/^((http|https):\/\/)?(?:www\.)?twitter\.com\/(\w+)?/i)) {
return LinkType.Twitter;
}
return LinkType.Url;
};
export const LinkDisplay = ({ value }: LinkDisplayProps) => {
const handleClick = (event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
};
const absoluteUrl = value?.url
? value.url.startsWith('http')
? value.url
: 'https://' + value.url
: '';
const displayedValue = value?.label || value?.url || '';
const type = checkUrlType(absoluteUrl);
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
return (
<EllipsisDisplay>
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}>
{displayedValue}
</SocialLink>
</EllipsisDisplay>
);
}
return (
<EllipsisDisplay>
<StyledRawLink href={absoluteUrl} onClick={handleClick}>
{displayedValue}
</StyledRawLink>
</EllipsisDisplay>
);
};
@@ -1,11 +0,0 @@
import { formatNumber } from '~/utils/format/number';
import { EllipsisDisplay } from './EllipsisDisplay';
type MoneyDisplayProps = {
value: number | null;
};
export const MoneyDisplay = ({ value }: MoneyDisplayProps) => (
<EllipsisDisplay>{value ? `$${formatNumber(value)}` : ''}</EllipsisDisplay>
);
@@ -1,11 +0,0 @@
import { formatNumber } from '~/utils/format/number';
import { EllipsisDisplay } from './EllipsisDisplay';
type NumberDisplayProps = {
value: string | number | null;
};
export const NumberDisplay = ({ value }: NumberDisplayProps) => (
<EllipsisDisplay>{value && formatNumber(Number(value))}</EllipsisDisplay>
);
@@ -1,27 +0,0 @@
import { MouseEvent } from 'react';
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
import { ContactLink } from '@/ui/navigation/link/components/ContactLink';
import { EllipsisDisplay } from './EllipsisDisplay';
type PhoneDisplayProps = {
value: string | null;
};
export const PhoneDisplay = ({ value }: PhoneDisplayProps) => (
<EllipsisDisplay>
{value && isValidPhoneNumber(value) ? (
<ContactLink
href={parsePhoneNumber(value, 'FR')?.getURI()}
onClick={(event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
}}
>
{parsePhoneNumber(value, 'FR')?.formatInternational() || value}
</ContactLink>
) : (
<ContactLink href="#">{value}</ContactLink>
)}
</EllipsisDisplay>
);
@@ -1,9 +0,0 @@
import { EllipsisDisplay } from './EllipsisDisplay';
type TextDisplayProps = {
text: string;
};
export const TextDisplay = ({ text }: TextDisplayProps) => (
<EllipsisDisplay>{text}</EllipsisDisplay>
);
@@ -1,72 +0,0 @@
import { MouseEvent } from 'react';
import styled from '@emotion/styled';
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
import {
LinkType,
SocialLink,
} from '@/ui/navigation/link/components/SocialLink';
import { EllipsisDisplay } from './EllipsisDisplay';
const StyledRawLink = styled(RoundedLink)`
overflow: hidden;
a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
type URLDisplayProps = {
value: string | null;
};
const checkUrlType = (url: string) => {
if (
/^(http|https):\/\/(?:www\.)?linkedin.com(\w+:{0,1}\w*@)?(\S+)(:([0-9])+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(
url,
)
) {
return LinkType.LinkedIn;
}
if (url.match(/^((http|https):\/\/)?(?:www\.)?twitter\.com\/(\w+)?/i)) {
return LinkType.Twitter;
}
return LinkType.Url;
};
export const URLDisplay = ({ value }: URLDisplayProps) => {
const handleClick = (event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
};
const absoluteUrl = value
? value.startsWith('http')
? value
: 'https://' + value
: '';
const displayedValue = value ?? '';
const type = checkUrlType(absoluteUrl);
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
return (
<EllipsisDisplay>
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}>
{displayedValue}
</SocialLink>
</EllipsisDisplay>
);
}
return (
<EllipsisDisplay>
<StyledRawLink href={absoluteUrl} onClick={handleClick}>
{displayedValue}
</StyledRawLink>
</EllipsisDisplay>
);
};
@@ -1,20 +0,0 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { PhoneDisplay } from '../PhoneDisplay'; // Adjust the import path as needed
const meta: Meta = {
title: 'UI/Input/PhoneInputDisplay/PhoneInputDisplay',
component: PhoneDisplay,
decorators: [ComponentWithRouterDecorator],
args: {
value: '+33788901234',
},
};
export default meta;
type Story = StoryObj<typeof PhoneDisplay>;
export const Default: Story = {};
@@ -1,29 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldBoolean } from '../../types/guards/isFieldBoolean';
export const useBooleanField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('BOOLEAN', isFieldBoolean, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<boolean>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
return {
fieldDefinition,
fieldValue,
setFieldValue,
hotkeyScope,
};
};
@@ -1,31 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { useRelationPicker } from '@/ui/input/components/internal/relation-picker/hooks/useRelationPicker';
import { entityFieldsFamilyState } from '@/ui/object/field/states/entityFieldsFamilyState';
import { isFieldFullName } from '@/ui/object/field/types/guards/isFieldFullName';
import { isFieldText } from '@/ui/object/field/types/guards/isFieldText';
import { FieldContext } from '../../contexts/FieldContext';
export const useChipField = () => {
const { entityId, fieldDefinition, basePathToShowPage } =
useContext(FieldContext);
const objectNameSingular =
isFieldText(fieldDefinition) || isFieldFullName(fieldDefinition)
? fieldDefinition.metadata.objectMetadataNameSingular
: undefined;
const record = useRecoilValue<any | null>(entityFieldsFamilyState(entityId));
const { identifiersMapper } = useRelationPicker();
return {
basePathToShowPage,
entityId,
objectNameSingular,
record,
identifiersMapper,
};
};
@@ -1,101 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldInitialValue } from '@/ui/object/field/types/FieldInitialValue';
import { canBeCastAsIntegerOrNull } from '~/utils/cast-as-integer-or-null';
import {
convertCurrencyMicrosToCurrency,
convertCurrencyToCurrencyMicros,
} from '~/utils/convert-currency-amount';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { usePersistField } from '../../hooks/usePersistField';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { FieldCurrencyValue } from '../../types/FieldMetadata';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldCurrency } from '../../types/guards/isFieldCurrency';
import { isFieldCurrencyValue } from '../../types/guards/isFieldCurrencyValue';
const initializeValue = (
fieldInitialValue: FieldInitialValue | undefined,
fieldValue: FieldCurrencyValue,
) => {
if (fieldInitialValue?.isEmpty) {
return { amount: null, currencyCode: 'USD' };
}
if (!isNaN(Number(fieldInitialValue?.value))) {
return {
amount: Number(fieldInitialValue?.value),
currencyCode: 'USD',
};
}
if (!fieldValue) {
return { amount: null, currencyCode: 'USD' };
}
return {
amount: convertCurrencyMicrosToCurrency(fieldValue.amountMicros),
currencyCode: fieldValue.currencyCode,
};
};
export const useCurrencyField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('CURRENCY', isFieldCurrency, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldCurrencyValue>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const persistField = usePersistField();
const persistCurrencyField = ({
amountText,
currencyCode,
}: {
amountText: string;
currencyCode: string;
}) => {
if (!canBeCastAsIntegerOrNull(amountText)) {
return;
}
const amount = parseFloat(amountText);
const newCurrencyValue = {
amountMicros: isNaN(amount)
? null
: convertCurrencyToCurrencyMicros(amount),
currencyCode: currencyCode,
};
if (!isFieldCurrencyValue(newCurrencyValue)) {
return;
}
persistField(newCurrencyValue);
};
const fieldInitialValue = useFieldInitialValue();
const initialValue = initializeValue(fieldInitialValue, fieldValue);
const initialAmount = initialValue.amount;
const initialCurrencyCode = initialValue.currencyCode;
return {
fieldDefinition,
fieldValue,
initialAmount,
initialCurrencyCode,
setFieldValue,
hotkeyScope,
persistCurrencyField,
};
};
@@ -1,29 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldDateTime } from '../../types/guards/isFieldDateTime';
export const useDateTimeField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('DATE_TIME', isFieldDateTime, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
return {
fieldDefinition,
fieldValue,
setFieldValue,
hotkeyScope,
};
};
@@ -1,37 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldEmail } from '../../types/guards/isFieldEmail';
export const useEmailField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('EMAIL', isFieldEmail, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const fieldInitialValue = useFieldInitialValue();
const initialValue = fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value ?? fieldValue;
return {
fieldDefinition,
fieldValue,
initialValue,
setFieldValue,
hotkeyScope,
};
};
@@ -1,51 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { usePersistField } from '../../hooks/usePersistField';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { FieldFullNameValue } from '../../types/FieldMetadata';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldFullName } from '../../types/guards/isFieldFullName';
import { isFieldFullNameValue } from '../../types/guards/isFieldFullNameValue';
export const useFullNameField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('FULL_NAME', isFieldFullName, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldFullNameValue>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const persistField = usePersistField();
const persistFullNameField = (newValue: FieldFullNameValue) => {
if (!isFieldFullNameValue(newValue)) {
return;
}
persistField(newValue);
};
const fieldInitialValue = useFieldInitialValue();
const initialValue: FieldFullNameValue = fieldInitialValue?.isEmpty
? { firstName: '', lastName: '' }
: fieldValue;
return {
fieldDefinition,
fieldValue,
initialValue,
setFieldValue,
hotkeyScope,
persistFullNameField,
};
};
@@ -1,53 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { usePersistField } from '../../hooks/usePersistField';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { FieldLinkValue } from '../../types/FieldMetadata';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldLink } from '../../types/guards/isFieldLink';
import { isFieldLinkValue } from '../../types/guards/isFieldLinkValue';
export const useLinkField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('LINK', isFieldLink, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldLinkValue>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const fieldInitialValue = useFieldInitialValue();
const initialValue: FieldLinkValue = fieldInitialValue?.isEmpty
? { url: '', label: '' }
: fieldInitialValue?.value
? { url: fieldInitialValue.value, label: '' }
: fieldValue;
const persistField = usePersistField();
const persistLinkField = (newValue: FieldLinkValue) => {
if (!isFieldLinkValue(newValue)) {
return;
}
persistField(newValue);
};
return {
fieldDefinition,
fieldValue,
initialValue,
setFieldValue,
hotkeyScope,
persistLinkField,
};
};
@@ -1,58 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import {
canBeCastAsIntegerOrNull,
castAsIntegerOrNull,
} from '~/utils/cast-as-integer-or-null';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { usePersistField } from '../../hooks/usePersistField';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldNumber } from '../../types/guards/isFieldNumber';
export const useNumberField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('NUMBER', isFieldNumber, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<number | null>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const persistField = usePersistField();
const persistNumberField = (newValue: string) => {
if (!canBeCastAsIntegerOrNull(newValue)) {
return;
}
const castedValue = castAsIntegerOrNull(newValue);
persistField(castedValue);
};
const fieldInitialValue = useFieldInitialValue();
const initialValue = fieldInitialValue?.isEmpty
? null
: !isNaN(Number(fieldInitialValue?.value))
? Number(fieldInitialValue?.value)
: null ?? fieldValue;
return {
fieldDefinition,
fieldValue,
initialValue,
setFieldValue,
hotkeyScope,
persistNumberField,
};
};
@@ -1,49 +0,0 @@
import { useContext } from 'react';
import { isPossiblePhoneNumber } from 'libphonenumber-js';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { usePersistField } from '../../hooks/usePersistField';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldPhone } from '../../types/guards/isFieldPhone';
export const usePhoneField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
//assertFieldMetadata('PHONE', isFieldPhone, fieldDefinition);
assertFieldMetadata('TEXT', isFieldPhone, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const persistField = usePersistField();
const persistPhoneField = (newPhoneValue: string) => {
if (!isPossiblePhoneNumber(newPhoneValue) && newPhoneValue !== '') return;
persistField(newPhoneValue);
};
const fieldInitialValue = useFieldInitialValue();
const initialValue = fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value ?? fieldValue;
return {
fieldDefinition,
fieldValue,
initialValue,
setFieldValue,
hotkeyScope,
persistPhoneField,
};
};
@@ -1,38 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '../../contexts/FieldContext';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { FieldRatingValue } from '../../types/FieldMetadata';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldRating } from '../../types/guards/isFieldRating';
export const useRatingField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata(
FieldMetadataType.Probability,
isFieldRating,
fieldDefinition,
);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldRatingValue | null>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const rating = +(fieldValue ?? 0);
return {
fieldDefinition,
rating,
setFieldValue,
hotkeyScope,
};
};
@@ -1,40 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldRelation } from '../../types/guards/isFieldRelation';
// TODO: we will be able to type more precisely when we will have custom field and custom entities support
export const useRelationField = () => {
const { entityId, fieldDefinition } = useContext(FieldContext);
assertFieldMetadata('RELATION', isFieldRelation, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<any | null>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const fieldInitialValue = useFieldInitialValue();
const initialSearchValue = fieldInitialValue?.isEmpty
? null
: fieldInitialValue?.value;
const initialValue = fieldInitialValue?.isEmpty ? null : fieldValue;
return {
fieldDefinition,
fieldValue,
initialValue,
initialSearchValue,
setFieldValue,
};
};
@@ -1,47 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { FieldSelectValue } from '../../types/FieldMetadata';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldSelect } from '../../types/guards/isFieldSelect';
import { isFieldSelectValue } from '../../types/guards/isFieldSelectValue';
export const useSelectField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('ENUM', isFieldSelect, fieldDefinition);
const { fieldName } = fieldDefinition.metadata;
const [fieldValue, setFieldValue] = useRecoilState<FieldSelectValue>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const fieldSelectValue = isFieldSelectValue(fieldValue)
? fieldValue
: { color: 'green' as ThemeColor, label: '' };
const fieldInitialValue = useFieldInitialValue();
const initialValue = {
color: 'green' as ThemeColor,
label: fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value ?? fieldSelectValue?.label ?? '',
};
return {
fieldDefinition,
fieldValue: fieldSelectValue,
initialValue,
setFieldValue,
hotkeyScope,
};
};
@@ -1,39 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldText } from '../../types/guards/isFieldText';
import { isFieldTextValue } from '../../types/guards/isFieldTextValue';
export const useTextField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('TEXT', isFieldText, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const fieldTextValue = isFieldTextValue(fieldValue) ? fieldValue : '';
const fieldInitialValue = useFieldInitialValue();
const initialValue = fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value ?? fieldTextValue;
return {
fieldDefinition,
fieldValue: fieldTextValue,
initialValue,
setFieldValue,
hotkeyScope,
};
};
@@ -1,40 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { isFieldUuid } from '@/ui/object/field/types/guards/isFieldUuid';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldTextValue } from '../../types/guards/isFieldTextValue';
export const useUuidField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('UUID', isFieldUuid, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const fieldTextValue = isFieldTextValue(fieldValue) ? fieldValue : '';
const fieldInitialValue = useFieldInitialValue();
const initialValue = fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value ?? fieldTextValue;
return {
fieldDefinition,
fieldValue: fieldTextValue,
initialValue,
setFieldValue,
hotkeyScope,
};
};
@@ -1,35 +0,0 @@
import { BooleanInput } from '@/ui/object/field/meta-types/input/components/internal/BooleanInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useBooleanField } from '../../hooks/useBooleanField';
import { FieldInputEvent } from './DateFieldInput';
export type BooleanFieldInputProps = {
onSubmit?: FieldInputEvent;
readonly?: boolean;
testId?: string;
};
export const BooleanFieldInput = ({
onSubmit,
readonly,
testId,
}: BooleanFieldInputProps) => {
const { fieldValue } = useBooleanField();
const persistField = usePersistField();
const handleToggle = (newValue: boolean) => {
onSubmit?.(() => persistField(newValue));
};
return (
<BooleanInput
value={fieldValue ?? ''}
onToggle={handleToggle}
readonly={readonly}
testId={testId}
/>
);
};
@@ -1,93 +0,0 @@
import { TextInput } from '@/ui/object/field/meta-types/input/components/internal/TextInput';
import { useCurrencyField } from '../../hooks/useCurrencyField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type CurrencyFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const CurrencyFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: CurrencyFieldInputProps) => {
const {
hotkeyScope,
initialAmount,
initialCurrencyCode,
persistCurrencyField,
} = useCurrencyField();
const handleEnter = (newValue: string) => {
onEnter?.(() => {
persistCurrencyField({
amountText: newValue,
currencyCode: initialCurrencyCode,
});
});
};
const handleEscape = (newValue: string) => {
onEscape?.(() => {
persistCurrencyField({
amountText: newValue,
currencyCode: initialCurrencyCode,
});
});
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newValue: string,
) => {
onClickOutside?.(() => {
persistCurrencyField({
amountText: newValue,
currencyCode: initialCurrencyCode,
});
});
};
const handleTab = (newValue: string) => {
onTab?.(() => {
persistCurrencyField({
amountText: newValue,
currencyCode: initialCurrencyCode,
});
});
};
const handleShiftTab = (newValue: string) => {
onShiftTab?.(() =>
persistCurrencyField({
amountText: newValue,
currencyCode: initialCurrencyCode,
}),
);
};
return (
<FieldInputOverlay>
<TextInput
value={initialAmount?.toString() ?? ''}
autoFocus
placeholder="Currency"
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};
@@ -1,62 +0,0 @@
import { DateInput } from '@/ui/object/field/meta-types/input/components/internal/DateInput';
import { Nullable } from '~/types/Nullable';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDateTimeField } from '../../hooks/useDateTimeField';
export type FieldInputEvent = (persist: () => void) => void;
export type DateFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const DateFieldInput = ({
onEnter,
onEscape,
onClickOutside,
}: DateFieldInputProps) => {
const { fieldValue, hotkeyScope } = useDateTimeField();
const persistField = usePersistField();
const persistDate = (newDate: Nullable<Date>) => {
if (!newDate) {
persistField('');
} else {
const newDateISO = newDate?.toISOString();
persistField(newDateISO);
}
};
const handleEnter = (newDate: Nullable<Date>) => {
onEnter?.(() => persistDate(newDate));
};
const handleEscape = (newDate: Nullable<Date>) => {
onEscape?.(() => persistDate(newDate));
};
const handleClickOutside = (
_event: MouseEvent | TouchEvent,
newDate: Nullable<Date>,
) => {
onClickOutside?.(() => persistDate(newDate));
};
const dateValue = fieldValue ? new Date(fieldValue) : null;
return (
<DateInput
hotkeyScope={hotkeyScope}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
value={dateValue}
/>
);
};
@@ -1,66 +0,0 @@
import { TextInput } from '@/ui/object/field/meta-types/input/components/internal/TextInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useEmailField } from '../../hooks/useEmailField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type EmailFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const EmailFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: EmailFieldInputProps) => {
const { fieldDefinition, initialValue, hotkeyScope } = useEmailField();
const persistField = usePersistField();
const handleEnter = (newText: string) => {
onEnter?.(() => persistField(newText));
};
const handleEscape = (newText: string) => {
onEscape?.(() => persistField(newText));
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newText: string,
) => {
onClickOutside?.(() => persistField(newText));
};
const handleTab = (newText: string) => {
onTab?.(() => persistField(newText));
};
const handleShiftTab = (newText: string) => {
onShiftTab?.(() => persistField(newText));
};
return (
<FieldInputOverlay>
<TextInput
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={initialValue}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};
@@ -1,74 +0,0 @@
import { useFullNameField } from '@/ui/object/field/meta-types/hooks/useFullNameField';
import { DoubleTextInput } from '@/ui/object/field/meta-types/input/components/internal/DoubleTextInput';
import { FieldDoubleText } from '@/ui/object/field/types/FieldDoubleText';
import { usePersistField } from '../../../hooks/usePersistField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type FullNameFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const FullNameFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: FullNameFieldInputProps) => {
const { hotkeyScope, initialValue } = useFullNameField();
const persistField = usePersistField();
const convertToFullName = (newDoubleText: FieldDoubleText) => {
return {
firstName: newDoubleText.firstValue,
lastName: newDoubleText.secondValue,
};
};
const handleEnter = (newDoubleText: FieldDoubleText) => {
onEnter?.(() => persistField(convertToFullName(newDoubleText)));
};
const handleEscape = (newDoubleText: FieldDoubleText) => {
onEscape?.(() => persistField(convertToFullName(newDoubleText)));
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newDoubleText: FieldDoubleText,
) => {
onClickOutside?.(() => persistField(convertToFullName(newDoubleText)));
};
const handleTab = (newDoubleText: FieldDoubleText) => {
onTab?.(() => persistField(convertToFullName(newDoubleText)));
};
const handleShiftTab = (newDoubleText: FieldDoubleText) => {
onShiftTab?.(() => persistField(convertToFullName(newDoubleText)));
};
return (
<FieldInputOverlay>
<DoubleTextInput
firstValue={initialValue.firstName}
secondValue={initialValue.lastName}
firstValuePlaceholder={'First name'}
secondValuePlaceholder={'Last name'}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};
@@ -1,88 +0,0 @@
import { TextInput } from '@/ui/object/field/meta-types/input/components/internal/TextInput';
import { useLinkField } from '../../hooks/useLinkField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type LinkFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const LinkFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: LinkFieldInputProps) => {
const { initialValue, hotkeyScope, persistLinkField } = useLinkField();
const handleEnter = (newURL: string) => {
onEnter?.(() =>
persistLinkField({
url: newURL,
label: newURL,
}),
);
};
const handleEscape = (newURL: string) => {
onEscape?.(() =>
persistLinkField({
url: newURL,
label: newURL,
}),
);
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newURL: string,
) => {
onClickOutside?.(() =>
persistLinkField({
url: newURL,
label: newURL,
}),
);
};
const handleTab = (newURL: string) => {
onTab?.(() =>
persistLinkField({
url: newURL,
label: newURL,
}),
);
};
const handleShiftTab = (newURL: string) => {
onShiftTab?.(() =>
persistLinkField({
url: newURL,
label: newURL,
}),
);
};
return (
<FieldInputOverlay>
<TextInput
value={initialValue.url}
autoFocus
placeholder="URL"
hotkeyScope={hotkeyScope}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onTab={handleTab}
onShiftTab={handleShiftTab}
/>
</FieldInputOverlay>
);
};
@@ -1,65 +0,0 @@
import { TextInput } from '@/ui/object/field/meta-types/input/components/internal/TextInput';
import { useNumberField } from '../../hooks/useNumberField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
export type FieldInputEvent = (persist: () => void) => void;
export type NumberFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const NumberFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: NumberFieldInputProps) => {
const { fieldDefinition, initialValue, hotkeyScope, persistNumberField } =
useNumberField();
const handleEnter = (newText: string) => {
onEnter?.(() => persistNumberField(newText));
};
const handleEscape = (newText: string) => {
onEscape?.(() => persistNumberField(newText));
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newText: string,
) => {
onClickOutside?.(() => persistNumberField(newText));
};
const handleTab = (newText: string) => {
onTab?.(() => persistNumberField(newText));
};
const handleShiftTab = (newText: string) => {
onShiftTab?.(() => persistNumberField(newText));
};
return (
<FieldInputOverlay>
<TextInput
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={initialValue?.toString() ?? ''}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};
@@ -1,64 +0,0 @@
import { PhoneInput } from '@/ui/object/field/meta-types/input/components/internal/PhoneInput';
import { usePhoneField } from '../../hooks/usePhoneField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type PhoneFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const PhoneFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: PhoneFieldInputProps) => {
const { fieldDefinition, initialValue, hotkeyScope, persistPhoneField } =
usePhoneField();
const handleEnter = (newText: string) => {
onEnter?.(() => persistPhoneField(newText));
};
const handleEscape = (newText: string) => {
onEscape?.(() => persistPhoneField(newText));
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newText: string,
) => {
onClickOutside?.(() => persistPhoneField(newText));
};
const handleTab = (newText: string) => {
onTab?.(() => persistPhoneField(newText));
};
const handleShiftTab = (newText: string) => {
onShiftTab?.(() => persistPhoneField(newText));
};
return (
<FieldInputOverlay>
<PhoneInput
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={initialValue}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};
@@ -1,28 +0,0 @@
import { RatingInput } from '@/ui/object/field/meta-types/input/components/internal/RatingInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useRatingField } from '../../hooks/useRatingField';
import { FieldInputEvent } from './DateFieldInput';
export type RatingFieldInputProps = {
onSubmit?: FieldInputEvent;
readonly?: boolean;
};
export const RatingFieldInput = ({
onSubmit,
readonly,
}: RatingFieldInputProps) => {
const { rating } = useRatingField();
const persistField = usePersistField();
const handleChange = (newRating: number) => {
onSubmit?.(() => persistField(`${newRating}`));
};
return (
<RatingInput value={rating} onChange={handleChange} readonly={readonly} />
);
};
@@ -1,65 +0,0 @@
import { useEffect } from 'react';
import styled from '@emotion/styled';
import { RelationPicker } from '@/ui/input/components/internal/relation-picker/components/RelationPicker';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { usePersistField } from '../../../hooks/usePersistField';
import { useRelationField } from '../../hooks/useRelationField';
import { FieldInputEvent } from './DateFieldInput';
const StyledRelationPickerContainer = styled.div`
left: -1px;
position: absolute;
top: -1px;
`;
export type RelationFieldInputProps = {
onSubmit?: FieldInputEvent;
onCancel?: () => void;
};
export const RelationFieldInput = ({
onSubmit,
onCancel,
}: RelationFieldInputProps) => {
const { fieldDefinition, initialValue, initialSearchValue } =
useRelationField();
const persistField = usePersistField();
const handleSubmit = (newEntity: EntityForSelect | null) => {
onSubmit?.(() => persistField(newEntity?.record ?? null));
};
useEffect(() => {}, [initialSearchValue]);
return (
<StyledRelationPickerContainer>
<RelationPicker
fieldDefinition={fieldDefinition}
recordId={initialValue?.id ?? ''}
onSubmit={handleSubmit}
onCancel={onCancel}
initialSearchFilter={initialSearchValue}
/>
{/* {fieldDefinition.metadata.fieldName === 'person' ? (
<PeoplePicker
personId={initialValue?.id ?? ''}
companyId={initialValue?.companyId ?? ''}
onSubmit={handleSubmit}
onCancel={onCancel}
initialSearchFilter={initialSearchValue}
/>
) : fieldDefinition.metadata.fieldName === 'company' ? (
<CompanyPicker
companyId={initialValue?.id ?? ''}
onSubmit={handleSubmit}
onCancel={onCancel}
initialSearchFilter={initialSearchValue}
/>
) : null} */}
</StyledRelationPickerContainer>
);
};
@@ -1,66 +0,0 @@
import { TextInput } from '@/ui/object/field/meta-types/input/components/internal/TextInput';
import { usePersistField } from '../../../hooks/usePersistField';
import { useTextField } from '../../hooks/useTextField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type TextFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const TextFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: TextFieldInputProps) => {
const { fieldDefinition, initialValue, hotkeyScope } = useTextField();
const persistField = usePersistField();
const handleEnter = (newText: string) => {
onEnter?.(() => persistField(newText));
};
const handleEscape = (newText: string) => {
onEscape?.(() => persistField(newText));
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newText: string,
) => {
onClickOutside?.(() => persistField(newText));
};
const handleTab = (newText: string) => {
onTab?.(() => persistField(newText));
};
const handleShiftTab = (newText: string) => {
onShiftTab?.(() => persistField(newText));
};
return (
<FieldInputOverlay>
<TextInput
placeholder={fieldDefinition.metadata.placeHolder}
autoFocus
value={initialValue}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};
@@ -1,100 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useBooleanField } from '../../../hooks/useBooleanField';
import {
BooleanFieldInput,
BooleanFieldInputProps,
} from '../BooleanFieldInput';
const BooleanFieldValueSetterEffect = ({ value }: { value: boolean }) => {
const { setFieldValue } = useBooleanField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type BooleanFieldInputWithContextProps = BooleanFieldInputProps & {
value: boolean;
entityId?: string;
};
const BooleanFieldInputWithContext = ({
value,
entityId,
onSubmit,
}: BooleanFieldInputWithContextProps) => {
return (
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'boolean',
label: 'Boolean',
iconName: 'Icon123',
type: 'BOOLEAN',
metadata: {
fieldName: 'Boolean',
},
}}
entityId={entityId}
>
<BooleanFieldValueSetterEffect value={value} />
<BooleanFieldInput onSubmit={onSubmit} testId="boolean-field-input" />
</FieldContextProvider>
);
};
const meta: Meta = {
title: 'UI/Data/Field/Input/BooleanFieldInput',
component: BooleanFieldInputWithContext,
args: {
value: true,
},
};
export default meta;
type Story = StoryObj<typeof BooleanFieldInputWithContext>;
const submitJestFn = jest.fn();
export const Default: Story = {};
export const Toggle: Story = {
args: {
onSubmit: submitJestFn,
},
argTypes: {
onSubmit: {
control: false,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByTestId('boolean-field-input');
const trueText = await within(input).findByText('True');
await expect(trueText).toBeInTheDocument();
await expect(submitJestFn).toHaveBeenCalledTimes(0);
await userEvent.click(input);
await expect(input).toHaveTextContent('False');
await expect(submitJestFn).toHaveBeenCalledTimes(1);
await userEvent.click(input);
await expect(input).toHaveTextContent('True');
await expect(submitJestFn).toHaveBeenCalledTimes(2);
},
};
@@ -1,131 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useDateTimeField } from '../../../hooks/useDateTimeField';
import { DateFieldInput, DateFieldInputProps } from '../DateFieldInput';
const formattedDate = new Date();
const DateFieldValueSetterEffect = ({ value }: { value: Date }) => {
const { setFieldValue } = useDateTimeField();
useEffect(() => {
setFieldValue(value.toISOString());
}, [setFieldValue, value]);
return <></>;
};
type DateFieldInputWithContextProps = DateFieldInputProps & {
value: Date;
entityId?: string;
};
const DateFieldInputWithContext = ({
value,
entityId,
onEscape,
onEnter,
onClickOutside,
}: DateFieldInputWithContextProps) => {
const setHotkeyScope = useSetHotkeyScope();
useEffect(() => {
setHotkeyScope('hotkey-scope');
}, [setHotkeyScope]);
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'date',
label: 'Date',
type: 'DATE_TIME',
iconName: 'IconCalendarEvent',
metadata: {
fieldName: 'Date',
},
}}
entityId={entityId}
>
<DateFieldValueSetterEffect value={value} />
<DateFieldInput
onEscape={onEscape}
onEnter={onEnter}
onClickOutside={onClickOutside}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div"></div>
</div>
);
};
const escapeJestFn = jest.fn();
const enterJestFn = jest.fn();
const clickOutsideJestFn = jest.fn();
const meta: Meta = {
title: 'UI/Data/Field/Input/DateFieldInput',
component: DateFieldInputWithContext,
args: {
value: formattedDate,
onEscape: escapeJestFn,
onEnter: enterJestFn,
onClickOutside: clickOutsideJestFn,
},
argTypes: {
onEscape: {
control: false,
},
onEnter: {
control: false,
},
onClickOutside: {
control: false,
},
},
};
export default meta;
type Story = StoryObj<typeof DateFieldInputWithContext>;
export const Default: Story = {};
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await userEvent.click(emptyDiv);
await expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
},
};
export const Escape: Story = {
play: async () => {
await expect(escapeJestFn).toHaveBeenCalledTimes(0);
await userEvent.keyboard('{esc}');
await expect(escapeJestFn).toHaveBeenCalledTimes(1);
},
};
export const Enter: Story = {
play: async () => {
await expect(enterJestFn).toHaveBeenCalledTimes(0);
await userEvent.keyboard('{enter}');
await expect(enterJestFn).toHaveBeenCalledTimes(1);
},
};
@@ -1,175 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useEmailField } from '../../../hooks/useEmailField';
import { EmailFieldInput, EmailFieldInputProps } from '../EmailFieldInput';
const EmailFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useEmailField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type EmailFieldInputWithContextProps = EmailFieldInputProps & {
value: string;
entityId?: string;
};
const EmailFieldInputWithContext = ({
entityId,
value,
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: EmailFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'email',
label: 'Email',
type: 'EMAIL',
iconName: 'IconLink',
metadata: {
fieldName: 'email',
placeHolder: 'username@email.com',
},
}}
entityId={entityId}
>
<EmailFieldValueSetterEffect value={value} />
<EmailFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</div>
);
};
const enterJestFn = jest.fn();
const escapeJestfn = jest.fn();
const clickOutsideJestFn = jest.fn();
const tabJestFn = jest.fn();
const shiftTabJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
enterJestFn.mockClear();
escapeJestfn.mockClear();
clickOutsideJestFn.mockClear();
tabJestFn.mockClear();
shiftTabJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/EmailFieldInput',
component: EmailFieldInputWithContext,
args: {
value: 'username@email.com',
onEnter: enterJestFn,
onEscape: escapeJestfn,
onClickOutside: clickOutsideJestFn,
onTab: tabJestFn,
onShiftTab: shiftTabJestFn,
},
argTypes: {
onEnter: { control: false },
onEscape: { control: false },
onClickOutside: { control: false },
onTab: { control: false },
onShiftTab: { control: false },
},
decorators: [clearMocksDecorator],
parameters: {
clearMocks: true,
},
};
export default meta;
type Story = StoryObj<typeof EmailFieldInputWithContext>;
export const Default: Story = {};
export const Enter: Story = {
play: async () => {
expect(enterJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{enter}');
expect(enterJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Escape: Story = {
play: async () => {
expect(escapeJestfn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{esc}');
expect(escapeJestfn).toHaveBeenCalledTimes(1);
});
},
};
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await waitFor(() => {
userEvent.click(emptyDiv);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Tab: Story = {
play: async () => {
expect(tabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{tab}');
expect(tabJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const ShiftTab: Story = {
play: async () => {
expect(shiftTabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{shift>}{tab}');
expect(shiftTabJestFn).toHaveBeenCalledTimes(1);
});
},
};
@@ -1,176 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useNumberField } from '../../../hooks/useNumberField';
import { NumberFieldInput, NumberFieldInputProps } from '../NumberFieldInput';
const NumberFieldValueSetterEffect = ({ value }: { value: number }) => {
const { setFieldValue } = useNumberField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type NumberFieldInputWithContextProps = NumberFieldInputProps & {
value: number;
entityId?: string;
};
const NumberFieldInputWithContext = ({
entityId,
value,
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: NumberFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'number',
label: 'Number',
iconName: 'Icon123',
type: 'NUMBER',
metadata: {
fieldName: 'number',
placeHolder: 'Enter number',
},
}}
entityId={entityId}
>
<NumberFieldValueSetterEffect value={value} />
<NumberFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</div>
);
};
const enterJestFn = jest.fn();
const escapeJestfn = jest.fn();
const clickOutsideJestFn = jest.fn();
const tabJestFn = jest.fn();
const shiftTabJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
enterJestFn.mockClear();
escapeJestfn.mockClear();
clickOutsideJestFn.mockClear();
tabJestFn.mockClear();
shiftTabJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/NumberFieldInput',
component: NumberFieldInputWithContext,
args: {
value: 1000,
isPositive: true,
onEnter: enterJestFn,
onEscape: escapeJestfn,
onClickOutside: clickOutsideJestFn,
onTab: tabJestFn,
onShiftTab: shiftTabJestFn,
},
argTypes: {
onEnter: { control: false },
onEscape: { control: false },
onClickOutside: { control: false },
onTab: { control: false },
onShiftTab: { control: false },
},
decorators: [clearMocksDecorator],
parameters: {
clearMocks: true,
},
};
export default meta;
type Story = StoryObj<typeof NumberFieldInputWithContext>;
export const Default: Story = {};
export const Enter: Story = {
play: async () => {
expect(enterJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{enter}');
expect(enterJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Escape: Story = {
play: async () => {
expect(escapeJestfn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{esc}');
expect(escapeJestfn).toHaveBeenCalledTimes(1);
});
},
};
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await waitFor(() => {
userEvent.click(emptyDiv);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Tab: Story = {
play: async () => {
expect(tabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{tab}');
expect(tabJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const ShiftTab: Story = {
play: async () => {
expect(shiftTabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{shift>}{tab}');
expect(shiftTabJestFn).toHaveBeenCalledTimes(1);
});
},
};
@@ -1,177 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { usePhoneField } from '../../../hooks/usePhoneField';
import { PhoneFieldInput, PhoneFieldInputProps } from '../PhoneFieldInput';
const PhoneFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = usePhoneField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type PhoneFieldInputWithContextProps = PhoneFieldInputProps & {
value: string;
entityId?: string;
};
const PhoneFieldInputWithContext = ({
entityId,
value,
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: PhoneFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'phone',
label: 'Phone',
type: 'TEXT',
iconName: 'IconPhone',
metadata: {
fieldName: 'phone',
placeHolder: 'Enter phone number',
objectMetadataNameSingular: 'person',
},
}}
entityId={entityId}
>
<PhoneFieldValueSetterEffect value={value} />
<PhoneFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</div>
);
};
const enterJestFn = jest.fn();
const escapeJestfn = jest.fn();
const clickOutsideJestFn = jest.fn();
const tabJestFn = jest.fn();
const shiftTabJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
enterJestFn.mockClear();
escapeJestfn.mockClear();
clickOutsideJestFn.mockClear();
tabJestFn.mockClear();
shiftTabJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/PhoneFieldInput',
component: PhoneFieldInputWithContext,
args: {
value: '+1-12-123-456',
isPositive: true,
onEnter: enterJestFn,
onEscape: escapeJestfn,
onClickOutside: clickOutsideJestFn,
onTab: tabJestFn,
onShiftTab: shiftTabJestFn,
},
argTypes: {
onEnter: { control: false },
onEscape: { control: false },
onClickOutside: { control: false },
onTab: { control: false },
onShiftTab: { control: false },
},
decorators: [clearMocksDecorator],
parameters: {
clearMocks: true,
},
};
export default meta;
type Story = StoryObj<typeof PhoneFieldInputWithContext>;
export const Default: Story = {};
export const Enter: Story = {
play: async () => {
expect(enterJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{enter}');
expect(enterJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Escape: Story = {
play: async () => {
expect(escapeJestfn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{esc}');
expect(escapeJestfn).toHaveBeenCalledTimes(1);
});
},
};
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await waitFor(() => {
userEvent.click(emptyDiv);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Tab: Story = {
play: async () => {
expect(tabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{tab}');
expect(tabJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const ShiftTab: Story = {
play: async () => {
expect(shiftTabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{shift>}{tab}');
expect(shiftTabJestFn).toHaveBeenCalledTimes(1);
});
},
};
@@ -1,107 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldRatingValue } from '../../../../types/FieldMetadata';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useRatingField } from '../../../hooks/useRatingField';
import { RatingFieldInput, RatingFieldInputProps } from '../RatingFieldInput';
const RatingFieldValueSetterEffect = ({
value,
}: {
value: FieldRatingValue;
}) => {
const { setFieldValue } = useRatingField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type RatingFieldInputWithContextProps = RatingFieldInputProps & {
value: FieldRatingValue;
entityId?: string;
};
const RatingFieldInputWithContext = ({
entityId,
value,
onSubmit,
}: RatingFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'rating',
label: 'Rating',
type: FieldMetadataType.Probability,
iconName: 'Icon123',
metadata: {
fieldName: 'Rating',
},
}}
entityId={entityId}
>
<RatingFieldValueSetterEffect value={value} />
<RatingFieldInput onSubmit={onSubmit} />
</FieldContextProvider>
);
};
const submitJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
submitJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/RatingFieldInput',
component: RatingFieldInputWithContext,
args: {
value: '3',
onSubmit: submitJestFn,
},
argTypes: {
onSubmit: { control: false },
},
decorators: [clearMocksDecorator],
parameters: {
clearMocks: true,
},
};
export default meta;
type Story = StoryObj<typeof RatingFieldInputWithContext>;
export const Default: Story = {};
export const Submit: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(submitJestFn).toHaveBeenCalledTimes(0);
const input = canvas.getByRole('slider', { name: 'Rating' });
const firstStar = input.firstElementChild;
if (firstStar) userEvent.click(firstStar);
expect(submitJestFn).toHaveBeenCalledTimes(1);
},
};
@@ -1,137 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { RelationPickerScope } from '@/ui/input/components/internal/relation-picker/scopes/RelationPickerScope';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useRelationField } from '../../../hooks/useRelationField';
import {
RelationFieldInput,
RelationFieldInputProps,
} from '../RelationFieldInput';
const RelationFieldValueSetterEffect = ({ value }: { value: number }) => {
const { setFieldValue } = useRelationField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type RelationFieldInputWithContextProps = RelationFieldInputProps & {
value: number;
entityId?: string;
};
const RelationFieldInputWithContext = ({
entityId,
value,
onSubmit,
onCancel,
}: RelationFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<div>
<RelationPickerScope relationPickerScopeId="relation-picker">
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'relation',
label: 'Relation',
type: 'RELATION',
iconName: 'IconLink',
metadata: {
fieldName: 'Relation',
},
}}
entityId={entityId}
>
<RelationFieldValueSetterEffect value={value} />
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
</FieldContextProvider>
</RelationPickerScope>
<div data-testid="data-field-input-click-outside-div" />
</div>
);
};
const submitJestFn = jest.fn();
const cancelJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
submitJestFn.mockClear();
cancelJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/RelationFieldInput',
component: RelationFieldInputWithContext,
args: {
useEditButton: true,
onSubmit: submitJestFn,
onCancel: cancelJestFn,
},
argTypes: {
onSubmit: { control: false },
onCancel: { control: false },
},
decorators: [clearMocksDecorator],
parameters: {
clearMocks: true,
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof RelationFieldInputWithContext>;
export const Default: Story = {
decorators: [ComponentWithRecoilScopeDecorator],
};
export const Submit: Story = {
decorators: [ComponentWithRecoilScopeDecorator],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(submitJestFn).toHaveBeenCalledTimes(0);
// FIXME: Failing because the picker is not fetching any items
const item = await canvas.findByText('Jane Doe');
userEvent.click(item);
expect(submitJestFn).toHaveBeenCalledTimes(1);
},
};
export const Cancel: Story = {
decorators: [ComponentWithRecoilScopeDecorator],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(cancelJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await waitFor(() => {
userEvent.click(emptyDiv);
expect(cancelJestFn).toHaveBeenCalledTimes(1);
});
},
};
@@ -1,175 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useTextField } from '../../../hooks/useTextField';
import { TextFieldInput, TextFieldInputProps } from '../TextFieldInput';
const TextFieldValueSetterEffect = ({ value }: { value: string }) => {
const { setFieldValue } = useTextField();
useEffect(() => {
setFieldValue(value);
}, [setFieldValue, value]);
return <></>;
};
type TextFieldInputWithContextProps = TextFieldInputProps & {
value: string;
entityId?: string;
};
const TextFieldInputWithContext = ({
entityId,
value,
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: TextFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'text',
label: 'Text',
type: 'TEXT',
iconName: 'IconTag',
metadata: {
fieldName: 'Text',
placeHolder: 'Enter text',
},
}}
entityId={entityId}
>
<TextFieldValueSetterEffect value={value} />
<TextFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</div>
);
};
const enterJestFn = jest.fn();
const escapeJestfn = jest.fn();
const clickOutsideJestFn = jest.fn();
const tabJestFn = jest.fn();
const shiftTabJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
enterJestFn.mockClear();
escapeJestfn.mockClear();
clickOutsideJestFn.mockClear();
tabJestFn.mockClear();
shiftTabJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/TextFieldInput',
component: TextFieldInputWithContext,
args: {
value: 'text',
onEnter: enterJestFn,
onEscape: escapeJestfn,
onClickOutside: clickOutsideJestFn,
onTab: tabJestFn,
onShiftTab: shiftTabJestFn,
},
argTypes: {
onEnter: { control: false },
onEscape: { control: false },
onClickOutside: { control: false },
onTab: { control: false },
onShiftTab: { control: false },
},
decorators: [clearMocksDecorator],
parameters: {
clearMocks: true,
},
};
export default meta;
type Story = StoryObj<typeof TextFieldInputWithContext>;
export const Default: Story = {};
export const Enter: Story = {
play: async () => {
expect(enterJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{enter}');
expect(enterJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Escape: Story = {
play: async () => {
expect(escapeJestfn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{esc}');
expect(escapeJestfn).toHaveBeenCalledTimes(1);
});
},
};
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await waitFor(() => {
userEvent.click(emptyDiv);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Tab: Story = {
play: async () => {
expect(tabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{tab}');
expect(tabJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const ShiftTab: Story = {
play: async () => {
expect(shiftTabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{shift>}{tab}');
expect(shiftTabJestFn).toHaveBeenCalledTimes(1);
});
},
};
@@ -1,61 +0,0 @@
import { useEffect, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck, IconX } from '@/ui/display/icon';
const StyledEditableBooleanFieldContainer = styled.div`
align-items: center;
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
display: flex;
height: 100%;
width: 100%;
`;
const StyledEditableBooleanFieldValue = styled.div`
margin-left: ${({ theme }) => theme.spacing(1)};
`;
type BooleanInputProps = {
value: boolean;
onToggle?: (newValue: boolean) => void;
readonly?: boolean;
testId?: string;
};
export const BooleanInput = ({
value,
onToggle,
readonly,
testId,
}: BooleanInputProps) => {
const [internalValue, setInternalValue] = useState(value);
useEffect(() => {
setInternalValue(value);
}, [value]);
const handleClick = () => {
setInternalValue(!internalValue);
onToggle?.(!internalValue);
};
const theme = useTheme();
return (
<StyledEditableBooleanFieldContainer
onClick={readonly ? undefined : handleClick}
data-testid={testId}
>
{internalValue ? (
<IconCheck size={theme.icon.size.sm} />
) : (
<IconX size={theme.icon.size.sm} />
)}
<StyledEditableBooleanFieldValue>
{internalValue ? 'True' : 'False'}
</StyledEditableBooleanFieldValue>
</StyledEditableBooleanFieldContainer>
);
};

Some files were not shown because too many files have changed in this diff Show More