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:
@@ -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';
|
||||
|
||||
|
||||
-97
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-28
@@ -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,
|
||||
};
|
||||
};
|
||||
-35
@@ -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,
|
||||
};
|
||||
};
|
||||
-21
@@ -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>
|
||||
);
|
||||
};
|
||||
-7
@@ -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>();
|
||||
-8
@@ -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,
|
||||
});
|
||||
-7
@@ -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,
|
||||
});
|
||||
-14
@@ -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[];
|
||||
};
|
||||
-24
@@ -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>
|
||||
);
|
||||
};
|
||||
-101
@@ -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,
|
||||
};
|
||||
};
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const RelationPickerRecoilScopeContext = createContext<string | null>(
|
||||
'relation-picker-context',
|
||||
);
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const relationPickerPreselectedIdScopedState = atomFamily<
|
||||
string,
|
||||
string
|
||||
>({
|
||||
key: 'relationPickerPreselectedIdScopedState',
|
||||
default: (param) => param,
|
||||
});
|
||||
-8
@@ -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 = {
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+1
-1
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-8
@@ -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} />;
|
||||
};
|
||||
-13
@@ -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} />;
|
||||
};
|
||||
-28
@@ -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} />;
|
||||
};
|
||||
-65
@@ -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 },
|
||||
},
|
||||
};
|
||||
-66
@@ -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 },
|
||||
},
|
||||
};
|
||||
-81
@@ -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,
|
||||
},
|
||||
};
|
||||
-68
@@ -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 },
|
||||
},
|
||||
};
|
||||
-67
@@ -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 },
|
||||
},
|
||||
};
|
||||
-10
@@ -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>;
|
||||
};
|
||||
-11
@@ -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>
|
||||
);
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
import { TextDisplay } from './TextDisplay';
|
||||
|
||||
export const DoubleTextDisplay = TextDisplay;
|
||||
-10
@@ -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 };
|
||||
-31
@@ -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>
|
||||
);
|
||||
-73
@@ -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>
|
||||
);
|
||||
};
|
||||
-11
@@ -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>
|
||||
);
|
||||
-11
@@ -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>
|
||||
);
|
||||
-27
@@ -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>
|
||||
);
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
type TextDisplayProps = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const TextDisplay = ({ text }: TextDisplayProps) => (
|
||||
<EllipsisDisplay>{text}</EllipsisDisplay>
|
||||
);
|
||||
-72
@@ -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>
|
||||
);
|
||||
};
|
||||
-20
@@ -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>
|
||||
);
|
||||
};
|
||||
-100
@@ -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);
|
||||
},
|
||||
};
|
||||
-131
@@ -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);
|
||||
},
|
||||
};
|
||||
-175
@@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
-176
@@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
-177
@@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
-107
@@ -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);
|
||||
},
|
||||
};
|
||||
-137
@@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
-175
@@ -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);
|
||||
});
|
||||
},
|
||||
};
|
||||
-61
@@ -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
Reference in New Issue
Block a user