FieldDisplay & FieldInput (#1708)

* Removed view field duplicate types

* wip

* wip 2

* wip 3

* Unified state for fields

* Renaming

* Wip

* Post merge

* Post post merge

* wip

* Delete unused file

* Boolean and Probability

* Finished InlineCell

* Renamed EditableCell to TableCell

* Finished double texts

* Finished MoneyField

* Fixed bug inline cell click outside

* Fixed hotkey scope

* Final fixes

* Phone

* Fix url and number input validation

* Fix

* Fix position

* wip refactor activity editor

* Fixed activity editor

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau 2023-09-27 18:18:02 +02:00 committed by GitHub
parent d9feabbc63
commit cbadcba188
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
290 changed files with 3152 additions and 4481 deletions

View File

@ -61,7 +61,8 @@
"uuid": "^9.0.0",
"web-vitals": "^2.1.4",
"xlsx-ugnis": "^0.19.3",
"yup": "^1.2.0"
"yup": "^1.2.0",
"zod": "^3.22.2"
},
"scripts": {
"start": "PORT=3001 craco start --max-warnings=0",

View File

@ -58,6 +58,7 @@ export const ActivityAssigneePicker = ({
lastName: user.lastName,
avatarType: 'rounded',
avatarUrl: user.avatarUrl ?? '',
originalEntity: user,
}),
selectedIds: activity?.accountOwner?.id ? [activity?.accountOwner?.id] : [],
});

View File

@ -8,9 +8,7 @@ import { ActivityComments } from '@/activities/components/ActivityComments';
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
import { GET_ACTIVITIES } from '@/activities/graphql/queries/getActivities';
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
import { EditableFieldHotkeyScope } from '@/ui/editable-field/types/EditableFieldHotkeyScope';
import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField';
import { IconCalendar } from '@/ui/icon/index';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import {
Activity,
@ -22,6 +20,7 @@ import {
import { debounce } from '~/utils/debounce';
import { ActivityAssigneeEditableField } from '../editable-fields/components/ActivityAssigneeEditableField';
import { ActivityEditorDateField } from '../editable-fields/components/ActivityEditorDateField';
import { ActivityRelationEditableField } from '../editable-fields/components/ActivityRelationEditableField';
import { ACTIVITY_UPDATE_FRAGMENT } from '../graphql/fragments/activityUpdateFragment';
import { CommentForDrawer } from '../types/CommentForDrawer';
@ -185,26 +184,12 @@ export const ActivityEditor = ({
<PropertyBox>
{activity.type === ActivityType.Task && (
<>
<DateEditableField
value={activity.dueAt}
Icon={IconCalendar}
label="Due date"
onSubmit={(newDate) => {
updateActivityMutation({
variables: {
where: {
id: activity.id,
},
data: {
dueAt: newDate,
},
},
refetchQueries: [getOperationName(GET_ACTIVITIES) ?? ''],
});
}}
hotkeyScope={EditableFieldHotkeyScope.EditableField}
/>
<ActivityAssigneeEditableField activity={activity} />
<RecoilScope>
<ActivityEditorDateField activityId={activity.id} />
</RecoilScope>
<RecoilScope>
<ActivityAssigneeEditableField activity={activity} />
</RecoilScope>
</>
)}
<ActivityRelationEditableField activity={activity} />

View File

@ -1,12 +1,11 @@
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
import { InlineCell } from '@/ui/editable-field/components/InlineCell';
import { EditableFieldHotkeyScope } from '@/ui/editable-field/types/EditableFieldHotkeyScope';
import { FieldContext } from '@/ui/field/contexts/FieldContext';
import { FieldDefinition } from '@/ui/field/types/FieldDefinition';
import { FieldRelationMetadata } from '@/ui/field/types/FieldMetadata';
import { IconUserCircle } from '@/ui/icon';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { UserChip } from '@/users/components/UserChip';
import { Company, User } from '~/generated/graphql';
import { ActivityAssigneeEditableFieldEditMode } from './ActivityAssigneeEditableFieldEditMode';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { Company, User, useUpdateActivityMutation } from '~/generated/graphql';
type OwnProps = {
activity: Pick<Company, 'id' | 'accountOwnerId'> & {
@ -16,32 +15,25 @@ type OwnProps = {
export const ActivityAssigneeEditableField = ({ activity }: OwnProps) => {
return (
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
<RecoilScope>
<EditableField
customEditHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,
}}
label="Assignee"
IconLabel={IconUserCircle}
editModeContent={
<ActivityAssigneeEditableFieldEditMode activity={activity} />
}
displayModeContent={
activity.assignee?.displayName ? (
<UserChip
id={activity.assignee.id}
name={activity.assignee?.displayName ?? ''}
pictureUrl={activity.assignee?.avatarUrl ?? ''}
/>
) : (
<></>
)
}
isDisplayModeContentEmpty={!activity.assignee}
isDisplayModeFixHeight={true}
/>
</RecoilScope>
</RecoilScope>
<FieldContext.Provider
value={{
entityId: activity.id,
recoilScopeId: 'assignee',
fieldDefinition: {
key: 'assignee',
name: 'Assignee',
Icon: IconUserCircle,
type: 'relation',
metadata: {
fieldName: 'assignee',
relationType: Entity.User,
},
} satisfies FieldDefinition<FieldRelationMetadata>,
useUpdateEntityMutation: useUpdateActivityMutation,
hotkeyScope: EditableFieldHotkeyScope.EditableField,
}}
>
<InlineCell />
</FieldContext.Provider>
);
};

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { ActivityAssigneePicker } from '@/activities/components/ActivityAssigneePicker';
import { useEditableField } from '@/ui/editable-field/hooks/useEditableField';
import { useInlineCell } from '@/ui/editable-field/hooks/useInlineCell';
import { Activity, User } from '~/generated/graphql';
const StyledContainer = styled.div`
@ -23,7 +23,7 @@ export const ActivityAssigneeEditableFieldEditMode = ({
onSubmit,
onCancel,
}: OwnProps) => {
const { closeEditableField } = useEditableField();
const { closeInlineCell: closeEditableField } = useInlineCell();
const handleSubmit = () => {
closeEditableField();

View File

@ -0,0 +1,38 @@
import { InlineCell } from '@/ui/editable-field/components/InlineCell';
import { EditableFieldHotkeyScope } from '@/ui/editable-field/types/EditableFieldHotkeyScope';
import { FieldContext } from '@/ui/field/contexts/FieldContext';
import { FieldDefinition } from '@/ui/field/types/FieldDefinition';
import { FieldDateMetadata } from '@/ui/field/types/FieldMetadata';
import { IconCalendar } from '@/ui/icon/index';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useUpdateActivityMutation } from '~/generated/graphql';
type OwnProps = {
activityId: string;
};
export const ActivityEditorDateField = ({ activityId }: OwnProps) => {
return (
<RecoilScope>
<FieldContext.Provider
value={{
entityId: activityId,
recoilScopeId: 'activityDueAt',
fieldDefinition: {
key: 'activityDueAt',
name: 'Due date',
Icon: IconCalendar,
type: 'date',
metadata: {
fieldName: 'dueAt',
},
} satisfies FieldDefinition<FieldDateMetadata>,
useUpdateEntityMutation: useUpdateActivityMutation,
hotkeyScope: EditableFieldHotkeyScope.EditableField,
}}
>
<InlineCell />
</FieldContext.Provider>
</RecoilScope>
);
};

View File

@ -1,5 +1,5 @@
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { InlineCellContainer } from '@/ui/editable-field/components/InlineCellContainer';
import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
import { IconArrowUpRight } from '@/ui/icon';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
@ -23,7 +23,7 @@ export const ActivityRelationEditableField = ({ activity }: OwnProps) => {
return (
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
<RecoilScope>
<EditableField
<InlineCellContainer
useEditButton
customEditHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,

View File

@ -5,7 +5,7 @@ import { useHandleCheckableActivityTargetChange } from '@/activities/hooks/useHa
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/activities/utils/flatMapAndSortEntityForSelectArrayByName';
import { useFilteredSearchCompanyQuery } from '@/companies/hooks/useFilteredSearchCompanyQuery';
import { useFilteredSearchPeopleQuery } from '@/people/hooks/useFilteredSearchPeopleQuery';
import { useEditableField } from '@/ui/editable-field/hooks/useEditableField';
import { useInlineCell } from '@/ui/editable-field/hooks/useInlineCell';
import { MultipleEntitySelect } from '@/ui/input/relation-picker/components/MultipleEntitySelect';
import { Activity, ActivityTarget } from '~/generated/graphql';
import { assertNotNull } from '~/utils/assert';
@ -88,7 +88,7 @@ export const ActivityRelationEditableFieldEditMode = ({
const handleCheckItemsChange = useHandleCheckableActivityTargetChange({
activity,
});
const { closeEditableField } = useEditableField();
const { closeInlineCell: closeEditableField } = useInlineCell();
const handleSubmit = useCallback(() => {
handleCheckItemsChange(selectedEntityIds, entitiesToSelect);

View File

@ -1,7 +1,9 @@
import React from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { ActivityEditor } from '@/activities/components/ActivityEditor';
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
import { useGetActivityQuery } from '~/generated/graphql';
import '@blocknote/core/style.css';
@ -27,12 +29,20 @@ export const RightDrawerActivity = ({
showComment = true,
autoFillTitle = false,
}: OwnProps) => {
const [, setEntityFields] = useRecoilState(
entityFieldsFamilyState(activityId),
);
const { data } = useGetActivityQuery({
variables: {
activityId: activityId ?? '',
},
skip: !activityId,
onCompleted: (data) => {
setEntityFields(data?.findManyActivities[0] ?? {});
},
});
const activity = data?.findManyActivities[0];
if (!activity) {

View File

@ -7,10 +7,9 @@ import { useBoardContext } from '@/ui/board/hooks/useBoardContext';
import { useCurrentCardSelected } from '@/ui/board/hooks/useCurrentCardSelected';
import { visibleBoardCardFieldsScopedSelector } from '@/ui/board/states/selectors/visibleBoardCardFieldsScopedSelector';
import { EntityChipVariant } from '@/ui/chip/components/EntityChip';
import { GenericEditableField } from '@/ui/editable-field/components/GenericEditableField';
import { EditableFieldDefinitionContext } from '@/ui/editable-field/contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '@/ui/editable-field/contexts/EditableFieldEntityIdContext';
import { EditableFieldMutationContext } from '@/ui/editable-field/contexts/EditableFieldMutationContext';
import { InlineCell } from '@/ui/editable-field/components/InlineCell';
import { EditableFieldHotkeyScope } from '@/ui/editable-field/types/EditableFieldHotkeyScope';
import { FieldContext } from '@/ui/field/contexts/FieldContext';
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useUpdateOnePipelineProgressMutation } from '~/generated/graphql';
@ -158,27 +157,28 @@ export const CompanyBoardCard = () => {
</StyledCheckboxContainer>
</StyledBoardCardHeader>
<StyledBoardCardBody>
<EditableFieldMutationContext.Provider
value={useUpdateOnePipelineProgressMutation}
>
<EditableFieldEntityIdContext.Provider value={boardCardId}>
{visibleBoardCardFields.map((viewField) => (
<PreventSelectOnClickContainer key={viewField.key}>
<EditableFieldDefinitionContext.Provider
value={{
key: viewField.key,
name: viewField.name,
Icon: viewField.Icon,
type: viewField.metadata.type,
metadata: viewField.metadata,
}}
>
<GenericEditableField />
</EditableFieldDefinitionContext.Provider>
</PreventSelectOnClickContainer>
))}
</EditableFieldEntityIdContext.Provider>
</EditableFieldMutationContext.Provider>
{visibleBoardCardFields.map((viewField) => (
<PreventSelectOnClickContainer key={viewField.key}>
<FieldContext.Provider
value={{
entityId: boardCardId,
recoilScopeId: boardCardId + viewField.key,
fieldDefinition: {
key: viewField.key,
name: viewField.name,
Icon: viewField.Icon,
type: viewField.type,
metadata: viewField.metadata,
useEditButton: viewField.useEditButton,
},
useUpdateEntityMutation: useUpdateOnePipelineProgressMutation,
hotkeyScope: EditableFieldHotkeyScope.EditableField,
}}
>
<InlineCell />
</FieldContext.Provider>
</PreventSelectOnClickContainer>
))}
</StyledBoardCardBody>
</StyledBoardCard>
</StyledBoardCardWrapper>

View File

@ -1,102 +0,0 @@
import { useFilteredSearchCompanyQuery } from '@/companies/hooks/useFilteredSearchCompanyQuery';
import { IconBuildingSkyscraper } from '@/ui/icon';
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 { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { isCreateModeScopedState } from '@/ui/table/editable-cell/states/isCreateModeScopedState';
import { DoubleTextCellEdit } from '@/ui/table/editable-cell/type/components/DoubleTextCellEdit';
import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useInsertOneCompanyMutation } from '~/generated/graphql';
export type OwnProps = {
companyId: string | null;
onSubmit: (newCompany: CompanyPickerSelectedCompany | null) => void;
onCancel?: () => void;
createModeEnabled?: boolean;
width?: number;
};
export type CompanyPickerSelectedCompany = EntityForSelect & {
domainName: string;
};
export const CompanyPickerCell = ({
companyId,
onSubmit,
onCancel,
createModeEnabled,
width,
}: OwnProps) => {
const [isCreateMode, setIsCreateMode] = useRecoilScopedState(
isCreateModeScopedState,
);
const [insertCompany] = useInsertOneCompanyMutation();
const [relationPickerSearchFilter] = useRecoilScopedState(
relationPickerSearchFilterScopedState,
);
const setHotkeyScope = useSetHotkeyScope();
const companies = useFilteredSearchCompanyQuery({
searchFilter: relationPickerSearchFilter,
selectedIds: [companyId ?? ''],
});
const handleCompanySelected = async (
company: CompanyPickerSelectedCompany | null | undefined,
) => {
onSubmit(company ?? null);
};
const handleStartCreation = () => {
setIsCreateMode(true);
setHotkeyScope(TableHotkeyScope.CellDoubleTextInput);
};
const handleCreate = async (firstValue: string, secondValue: string) => {
const insertCompanyRequest = await insertCompany({
variables: {
data: {
name: firstValue,
domainName: secondValue,
address: '',
},
},
});
const companyCreated = insertCompanyRequest.data?.createOneCompany;
companyCreated &&
onSubmit({
id: companyCreated.id,
name: companyCreated.name,
entityType: Entity.Company,
domainName: companyCreated.domainName,
});
setIsCreateMode(false);
};
return isCreateMode ? (
<DoubleTextCellEdit
firstValue={relationPickerSearchFilter}
secondValue=""
firstValuePlaceholder="Name"
secondValuePlaceholder="Url"
onSubmit={handleCreate}
/>
) : (
<SingleEntitySelect
EmptyIcon={IconBuildingSkyscraper}
emptyLabel="No Company"
entitiesToSelect={companies.entitiesToSelect}
loading={companies.loading}
onCancel={onCancel}
onCreate={createModeEnabled ? handleStartCreation : undefined}
onEntitySelected={handleCompanySelected}
selectedEntity={companies.selectedEntities[0]}
width={width}
/>
);
};

View File

@ -1,14 +1,14 @@
import {
ViewFieldBooleanMetadata,
ViewFieldChipMetadata,
ViewFieldDateMetadata,
ViewFieldMetadata,
ViewFieldMoneyMetadata,
ViewFieldNumberMetadata,
ViewFieldRelationMetadata,
ViewFieldTextMetadata,
ViewFieldURLMetadata,
} from '@/ui/editable-field/types/ViewField';
FieldBooleanMetadata,
FieldChipMetadata,
FieldDateMetadata,
FieldMetadata,
FieldMoneyMetadata,
FieldNumberMetadata,
FieldRelationMetadata,
FieldTextMetadata,
FieldURLMetadata,
} from '@/ui/field/types/FieldMetadata';
import {
IconBrandLinkedin,
IconBrandX,
@ -24,7 +24,7 @@ import {
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { ColumnDefinition } from '@/ui/table/types/ColumnDefinition';
export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadata>[] =
export const companiesAvailableColumnDefinitions: ColumnDefinition<FieldMetadata>[] =
[
{
key: 'name',
@ -32,125 +32,131 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMeta
Icon: IconBuildingSkyscraper,
size: 180,
index: 0,
type: 'chip',
metadata: {
type: 'chip',
urlFieldName: 'domainName',
contentFieldName: 'name',
relationType: Entity.Company,
placeHolder: 'Company Name',
},
isVisible: true,
} as ColumnDefinition<ViewFieldChipMetadata>,
} satisfies ColumnDefinition<FieldChipMetadata>,
{
key: 'domainName',
name: 'URL',
Icon: IconLink,
size: 100,
index: 1,
type: 'url',
metadata: {
type: 'url',
fieldName: 'domainName',
placeHolder: 'example.com',
},
isVisible: true,
} as ColumnDefinition<ViewFieldURLMetadata>,
useEditButton: true,
} satisfies ColumnDefinition<FieldURLMetadata>,
{
key: 'accountOwner',
name: 'Account Owner',
Icon: IconUserCircle,
size: 150,
index: 2,
type: 'relation',
metadata: {
type: 'relation',
fieldName: 'accountOwner',
relationType: Entity.User,
},
isVisible: true,
} satisfies ColumnDefinition<ViewFieldRelationMetadata>,
} satisfies ColumnDefinition<FieldRelationMetadata>,
{
key: 'createdAt',
name: 'Creation',
Icon: IconCalendarEvent,
size: 150,
index: 3,
type: 'date',
metadata: {
type: 'date',
fieldName: 'createdAt',
},
isVisible: true,
} satisfies ColumnDefinition<ViewFieldDateMetadata>,
} satisfies ColumnDefinition<FieldDateMetadata>,
{
key: 'employees',
name: 'Employees',
Icon: IconUsers,
size: 150,
index: 4,
type: 'number',
metadata: {
type: 'number',
fieldName: 'employees',
isPositive: true,
placeHolder: 'Employees',
},
isVisible: true,
} satisfies ColumnDefinition<ViewFieldNumberMetadata>,
} satisfies ColumnDefinition<FieldNumberMetadata>,
{
key: 'linkedin',
name: 'LinkedIn',
Icon: IconBrandLinkedin,
size: 170,
index: 5,
type: 'url',
metadata: {
type: 'url',
fieldName: 'linkedinUrl',
placeHolder: 'LinkedIn URL',
},
isVisible: true,
} satisfies ColumnDefinition<ViewFieldURLMetadata>,
useEditButton: true,
} satisfies ColumnDefinition<FieldURLMetadata>,
{
key: 'address',
name: 'Address',
Icon: IconMap,
size: 170,
index: 6,
type: 'text',
metadata: {
type: 'text',
fieldName: 'address',
placeHolder: 'Address', // Hack: Fake character to prevent password-manager from filling the field
},
isVisible: true,
} satisfies ColumnDefinition<ViewFieldTextMetadata>,
} satisfies ColumnDefinition<FieldTextMetadata>,
{
key: 'idealCustomerProfile',
name: 'ICP',
Icon: IconTarget,
size: 150,
index: 7,
type: 'boolean',
metadata: {
type: 'boolean',
fieldName: 'idealCustomerProfile',
},
isVisible: false,
} satisfies ColumnDefinition<ViewFieldBooleanMetadata>,
} satisfies ColumnDefinition<FieldBooleanMetadata>,
{
key: 'annualRecurringRevenue',
name: 'ARR',
Icon: IconMoneybag,
size: 150,
index: 8,
type: 'moneyAmount',
metadata: {
type: 'moneyAmount',
fieldName: 'annualRecurringRevenue',
placeHolder: 'ARR',
},
} satisfies ColumnDefinition<ViewFieldMoneyMetadata>,
} satisfies ColumnDefinition<FieldMoneyMetadata>,
{
key: 'xUrl',
name: 'Twitter',
Icon: IconBrandX,
size: 150,
index: 9,
type: 'url',
metadata: {
type: 'url',
fieldName: 'xUrl',
placeHolder: 'X',
},
isVisible: false,
} satisfies ColumnDefinition<ViewFieldURLMetadata>,
useEditButton: true,
} satisfies ColumnDefinition<FieldURLMetadata>,
];

View File

@ -1,12 +1,11 @@
import { useSetRecoilState } from 'recoil';
import { genericEntitiesFamilyState } from '@/ui/editable-field/states/genericEntitiesFamilyState';
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
import { useGetCompanyQuery } from '~/generated/graphql';
export const useCompanyQuery = (id: string) => {
const updateCompanyShowPage = useSetRecoilState(
genericEntitiesFamilyState(id),
);
const updateCompanyShowPage = useSetRecoilState(entityFieldsFamilyState(id));
return useGetCompanyQuery({
variables: { where: { id } },
onCompleted: (data) => {

View File

@ -28,6 +28,7 @@ export const useFilteredSearchCompanyQuery = ({
avatarUrl: getLogoUrlFromDomainName(company.domainName),
domainName: company.domainName,
avatarType: 'squared',
originalEntity: company,
}),
selectedIds: selectedIds,
limit,

View File

@ -5,7 +5,7 @@ import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardId
import { boardColumnsState } from '@/ui/board/states/boardColumnsState';
import { savedBoardColumnsState } from '@/ui/board/states/savedBoardColumnsState';
import { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition';
import { genericEntitiesFamilyState } from '@/ui/editable-field/states/genericEntitiesFamilyState';
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
import { isThemeColor } from '@/ui/theme/utils/castStringAsThemeColor';
import { Pipeline } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
@ -72,10 +72,7 @@ export const useUpdateCompanyBoard = () =>
if (!isDeeplyEqual(currentCompanyProgress, companyProgress)) {
set(companyProgressesFamilyState(id), companyProgress);
set(
genericEntitiesFamilyState(id),
companyProgress.pipelineProgress,
);
set(entityFieldsFamilyState(id), companyProgress.pipelineProgress);
}
}

View File

@ -15,7 +15,7 @@ export const CompanyTableMockMode = () => {
ViewBarRecoilScopeContext: TableRecoilScopeContext,
}}
>
<EntityTable updateEntityMutation={[useUpdateOneCompanyMutation()]} />
<EntityTable updateEntityMutation={useUpdateOneCompanyMutation} />
</ViewBarContext.Provider>
</>
);

View File

@ -55,6 +55,7 @@ export const PeoplePicker = ({
name: `${person.firstName} ${person.lastName}`,
avatarType: 'rounded',
avatarUrl: person.avatarUrl ?? '',
originalEntity: person,
}),
orderByField: 'firstName',
excludeEntityIds: excludePersonIds,

View File

@ -1,13 +1,13 @@
import {
ViewFieldDateMetadata,
ViewFieldDoubleTextChipMetadata,
ViewFieldEmailMetadata,
ViewFieldMetadata,
ViewFieldPhoneMetadata,
ViewFieldRelationMetadata,
ViewFieldTextMetadata,
ViewFieldURLMetadata,
} from '@/ui/editable-field/types/ViewField';
FieldDateMetadata,
FieldDoubleTextChipMetadata,
FieldEmailMetadata,
FieldMetadata,
FieldPhoneMetadata,
FieldRelationMetadata,
FieldTextMetadata,
FieldURLMetadata,
} from '@/ui/field/types/FieldMetadata';
import {
IconBrandLinkedin,
IconBrandX,
@ -22,7 +22,7 @@ import {
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { ColumnDefinition } from '@/ui/table/types/ColumnDefinition';
export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadata>[] =
export const peopleAvailableColumnDefinitions: ColumnDefinition<FieldMetadata>[] =
[
{
key: 'displayName',
@ -30,8 +30,8 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadat
Icon: IconUser,
size: 210,
index: 0,
type: 'double-text-chip',
metadata: {
type: 'double-text-chip',
firstValueFieldName: 'firstName',
secondValueFieldName: 'lastName',
firstValuePlaceholder: 'First name', // Hack: Fake character to prevent password-manager from filling the field
@ -39,100 +39,104 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadat
avatarUrlFieldName: 'avatarUrl',
entityType: Entity.Person,
},
} satisfies ColumnDefinition<ViewFieldDoubleTextChipMetadata>,
} satisfies ColumnDefinition<FieldDoubleTextChipMetadata>,
{
key: 'email',
name: 'Email',
Icon: IconMail,
size: 150,
type: 'email',
index: 1,
metadata: {
type: 'email',
fieldName: 'email',
placeHolder: 'Email', // Hack: Fake character to prevent password-manager from filling the field
},
} satisfies ColumnDefinition<ViewFieldEmailMetadata>,
useEditButton: true,
} satisfies ColumnDefinition<FieldEmailMetadata>,
{
key: 'company',
name: 'Company',
Icon: IconBuildingSkyscraper,
size: 150,
index: 2,
type: 'relation',
metadata: {
type: 'relation',
fieldName: 'company',
relationType: Entity.Company,
},
} satisfies ColumnDefinition<ViewFieldRelationMetadata>,
} satisfies ColumnDefinition<FieldRelationMetadata>,
{
key: 'phone',
name: 'Phone',
Icon: IconPhone,
size: 150,
index: 3,
type: 'phone',
metadata: {
type: 'phone',
fieldName: 'phone',
placeHolder: 'Phone', // Hack: Fake character to prevent password-manager from filling the field
},
} satisfies ColumnDefinition<ViewFieldPhoneMetadata>,
useEditButton: true,
} satisfies ColumnDefinition<FieldPhoneMetadata>,
{
key: 'createdAt',
name: 'Creation',
Icon: IconCalendarEvent,
size: 150,
index: 4,
type: 'date',
metadata: {
type: 'date',
fieldName: 'createdAt',
},
} satisfies ColumnDefinition<ViewFieldDateMetadata>,
} satisfies ColumnDefinition<FieldDateMetadata>,
{
key: 'city',
name: 'City',
Icon: IconMap,
size: 150,
index: 5,
type: 'text',
metadata: {
type: 'text',
fieldName: 'city',
placeHolder: 'City', // Hack: Fake character to prevent password-manager from filling the field
},
} satisfies ColumnDefinition<ViewFieldTextMetadata>,
} satisfies ColumnDefinition<FieldTextMetadata>,
{
key: 'jobTitle',
name: 'Job title',
Icon: IconBriefcase,
size: 150,
index: 6,
type: 'text',
metadata: {
type: 'text',
fieldName: 'jobTitle',
placeHolder: 'Job title',
},
} satisfies ColumnDefinition<ViewFieldTextMetadata>,
} satisfies ColumnDefinition<FieldTextMetadata>,
{
key: 'linkedin',
name: 'LinkedIn',
Icon: IconBrandLinkedin,
size: 150,
index: 7,
type: 'url',
metadata: {
type: 'url',
fieldName: 'linkedinUrl',
placeHolder: 'LinkedIn',
},
} satisfies ColumnDefinition<ViewFieldURLMetadata>,
useEditButton: true,
} satisfies ColumnDefinition<FieldURLMetadata>,
{
key: 'x',
name: 'Twitter',
Icon: IconBrandX,
size: 150,
index: 8,
type: 'url',
metadata: {
type: 'url',
fieldName: 'xUrl',
placeHolder: 'X',
},
} satisfies ColumnDefinition<ViewFieldURLMetadata>,
useEditButton: true,
} satisfies ColumnDefinition<FieldURLMetadata>,
];

View File

@ -1,7 +1,7 @@
import { useState } from 'react';
import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
import { DoubleTextInputEdit } from '@/ui/input/components/DoubleTextInputEdit';
import { EntityTitleDoubleTextInput } from '@/ui/input/components/EntityTitleDoubleTextInput';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { Person, useUpdateOnePersonMutation } from '~/generated/graphql';
@ -47,7 +47,7 @@ export const PeopleFullNameEditableField = ({ people }: OwnProps) => {
return (
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
<DoubleTextInputEdit
<EntityTitleDoubleTextInput
firstValuePlaceholder="Empty"
secondValuePlaceholder="Empty"
firstValue={internalValueFirstName ?? ''}

View File

@ -5,8 +5,8 @@ import {
ActivityTargetableEntity,
ActivityTargetableEntityType,
} from '@/activities/types/ActivityTargetableEntity';
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
import { selectedRowIdsSelector } from '@/ui/table/states/selectors/selectedRowIdsSelector';
import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState';
import { ActivityType, Person } from '~/generated/graphql';
export const useCreateActivityForPeople = () => {
@ -20,7 +20,7 @@ export const useCreateActivityForPeople = () => {
const relatedEntites: ActivityTargetableEntity[] = [];
for (const id of selectedRowIds) {
const person = snapshot
.getLoadable(tableEntitiesFamilyState(id))
.getLoadable(entityFieldsFamilyState(id))
.getValue() as Person;
if (
person?.company?.id &&

View File

@ -1,12 +1,11 @@
import { useSetRecoilState } from 'recoil';
import { genericEntitiesFamilyState } from '@/ui/editable-field/states/genericEntitiesFamilyState';
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
import { useGetPersonQuery } from '~/generated/graphql';
export const usePersonQuery = (id: string) => {
const updatePersonShowPage = useSetRecoilState(
genericEntitiesFamilyState(id),
);
const updatePersonShowPage = useSetRecoilState(entityFieldsFamilyState(id));
return useGetPersonQuery({
variables: { id },
onCompleted: (data) => {

View File

@ -1,11 +1,11 @@
import { BoardFieldDefinition } from '@/ui/board/types/BoardFieldDefinition';
import {
ViewFieldDateMetadata,
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldNumberMetadata,
ViewFieldProbabilityMetadata,
ViewFieldRelationMetadata,
} from '@/ui/editable-field/types/ViewField';
FieldDateMetadata,
FieldMetadata,
FieldNumberMetadata,
FieldProbabilityMetadata,
FieldRelationMetadata,
} from '@/ui/field/types/FieldMetadata';
import {
IconCalendarEvent,
IconCurrencyDollar,
@ -14,52 +14,54 @@ import {
} from '@/ui/icon';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
export const pipelineAvailableFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[] =
export const pipelineAvailableFieldDefinitions: BoardFieldDefinition<FieldMetadata>[] =
[
{
key: 'closeDate',
name: 'Close Date',
Icon: IconCalendarEvent,
index: 0,
type: 'date',
metadata: {
type: 'date',
fieldName: 'closeDate',
},
isVisible: true,
} satisfies ViewFieldDefinition<ViewFieldDateMetadata>,
} satisfies BoardFieldDefinition<FieldDateMetadata>,
{
key: 'amount',
name: 'Amount',
Icon: IconCurrencyDollar,
index: 1,
type: 'number',
metadata: {
type: 'number',
fieldName: 'amount',
placeHolder: '0',
},
isVisible: true,
} satisfies ViewFieldDefinition<ViewFieldNumberMetadata>,
} satisfies BoardFieldDefinition<FieldNumberMetadata>,
{
key: 'probability',
name: 'Probability',
Icon: IconProgressCheck,
index: 2,
type: 'probability',
metadata: {
type: 'probability',
fieldName: 'probability',
},
isVisible: true,
} satisfies ViewFieldDefinition<ViewFieldProbabilityMetadata>,
} satisfies BoardFieldDefinition<FieldProbabilityMetadata>,
{
key: 'pointOfContact',
name: 'Point of Contact',
Icon: IconUser,
index: 3,
type: 'relation',
metadata: {
type: 'relation',
fieldName: 'pointOfContact',
relationType: Entity.Person,
useEditButton: true,
},
isVisible: true,
} satisfies ViewFieldDefinition<ViewFieldRelationMetadata>,
useEditButton: true,
} satisfies BoardFieldDefinition<FieldRelationMetadata>,
];

View File

@ -1,39 +1,26 @@
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { ViewFieldForVisibility } from '@/ui/view-bar/types/ViewFieldForVisibility';
import { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState';
import { boardCardFieldsByKeyScopedSelector } from '../states/selectors/boardCardFieldsByKeyScopedSelector';
import { useBoardContext } from './useBoardContext';
export const useBoardCardFields = () => {
const { BoardRecoilScopeContext } = useBoardContext();
const [boardCardFields, setBoardCardFields] = useRecoilScopedState(
const [, setBoardCardFields] = useRecoilScopedState(
boardCardFieldsScopedState,
BoardRecoilScopeContext,
);
const boardCardFieldsByKey = useRecoilScopedValue(
boardCardFieldsByKeyScopedSelector,
BoardRecoilScopeContext,
);
const handleFieldVisibilityChange = (
field: ViewFieldDefinition<ViewFieldMetadata>,
) => {
const nextFields = boardCardFieldsByKey[field.key]
? boardCardFields.map((previousField) =>
previousField.key === field.key
? { ...previousField, isVisible: !field.isVisible }
: previousField,
)
: [...boardCardFields, { ...field, isVisible: true }];
setBoardCardFields(nextFields);
const handleFieldVisibilityChange = (field: ViewFieldForVisibility) => {
setBoardCardFields((previousFields) =>
previousFields.map((previousField) =>
previousField.key === field.key
? { ...previousField, isVisible: !field.isVisible }
: previousField,
),
);
};
return { handleFieldVisibilityChange };

View File

@ -1,12 +1,11 @@
import { atomFamily } from 'recoil';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { FieldMetadata } from '@/ui/field/types/FieldMetadata';
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
export const availableBoardCardFieldsScopedState = atomFamily<
ViewFieldDefinition<ViewFieldMetadata>[],
BoardFieldDefinition<FieldMetadata>[],
string
>({
key: 'availableBoardCardFieldsScopedState',

View File

@ -1,12 +1,11 @@
import { atomFamily } from 'recoil';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { FieldMetadata } from '@/ui/field/types/FieldMetadata';
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
export const boardCardFieldsScopedState = atomFamily<
ViewFieldDefinition<ViewFieldMetadata>[],
BoardFieldDefinition<FieldMetadata>[],
string
>({
key: 'boardCardFieldsScopedState',

View File

@ -1,12 +1,11 @@
import { atomFamily } from 'recoil';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { FieldMetadata } from '@/ui/field/types/FieldMetadata';
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
export const savedBoardCardFieldsFamilyState = atomFamily<
ViewFieldDefinition<ViewFieldMetadata>[],
BoardFieldDefinition<FieldMetadata>[],
string | undefined
>({
key: 'savedBoardCardFieldsFamilyState',

View File

@ -1,10 +1,8 @@
import { selectorFamily } from 'recoil';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { FieldMetadata } from '@/ui/field/types/FieldMetadata';
import { BoardFieldDefinition } from '../../types/BoardFieldDefinition';
import { boardCardFieldsScopedState } from '../boardCardFieldsScopedState';
export const boardCardFieldsByKeyScopedSelector = selectorFamily({
@ -13,6 +11,6 @@ export const boardCardFieldsByKeyScopedSelector = selectorFamily({
(scopeId: string) =>
({ get }) =>
get(boardCardFieldsScopedState(scopeId)).reduce<
Record<string, ViewFieldDefinition<ViewFieldMetadata>>
Record<string, BoardFieldDefinition<FieldMetadata>>
>((result, field) => ({ ...result, [field.key]: field }), {}),
});

View File

@ -1,10 +1,8 @@
import { selectorFamily } from 'recoil';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { FieldMetadata } from '@/ui/field/types/FieldMetadata';
import { BoardFieldDefinition } from '../../types/BoardFieldDefinition';
import { savedBoardCardFieldsFamilyState } from '../savedBoardCardFieldsFamilyState';
export const savedBoardCardFieldsByKeyFamilySelector = selectorFamily({
@ -13,6 +11,6 @@ export const savedBoardCardFieldsByKeyFamilySelector = selectorFamily({
(viewId: string | undefined) =>
({ get }) =>
get(savedBoardCardFieldsFamilyState(viewId)).reduce<
Record<string, ViewFieldDefinition<ViewFieldMetadata>>
Record<string, BoardFieldDefinition<FieldMetadata>>
>((result, field) => ({ ...result, [field.key]: field }), {}),
});

View File

@ -0,0 +1,8 @@
import { FieldDefinition } from '@/ui/field/types/FieldDefinition';
import { FieldMetadata } from '@/ui/field/types/FieldMetadata';
export type BoardFieldDefinition<T extends FieldMetadata> =
FieldDefinition<T> & {
index: number;
isVisible?: boolean;
};

View File

@ -1,6 +1,7 @@
import { useRef } from 'react';
import { Keys } from 'react-hotkeys-hook';
import { flip, offset, Placement, useFloating } from '@floating-ui/react';
import { Key } from 'ts-key-enum';
import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -71,7 +72,7 @@ export const DropdownButton = ({
});
useScopedHotkeys(
'esc',
Key.Escape,
() => {
closeDropdownButton();
},

View File

@ -1,27 +0,0 @@
import { useContext } from 'react';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { FieldRecoilScopeContext } from '../states/recoil-scope-contexts/FieldRecoilScopeContext';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldBooleanMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField';
import { GenericEditableBooleanFieldDisplayMode } from './GenericEditableBooleanFieldDisplayMode';
export const GenericEditableBooleanField = () => {
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldBooleanMetadata>;
return (
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
<EditableField
IconLabel={currentEditableFieldDefinition.Icon}
displayModeContent={<GenericEditableBooleanFieldDisplayMode />}
displayModeContentOnly
/>
</RecoilScope>
);
};

View File

@ -1,44 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { BooleanInput } from '@/ui/input/components/BooleanInput';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldBooleanMetadata } from '../types/FieldMetadata';
export const GenericEditableBooleanFieldDisplayMode = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldBooleanMetadata>;
const [fieldValue, setFieldValue] = useRecoilState<boolean>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const updateField = useUpdateGenericEntityField();
const handleSubmit = (newValue: boolean) => {
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
newValue,
);
// TODO: use optimistic effect instead, but needs generic refactor
setFieldValue(newValue);
}
};
return <BooleanInput value={fieldValue} onToggle={handleSubmit} />;
};

View File

@ -1,42 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { DateDisplay } from '@/ui/content-display/components/DateDisplay';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { FieldRecoilScopeContext } from '../states/recoil-scope-contexts/FieldRecoilScopeContext';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldDateMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField';
import { GenericEditableDateFieldEditMode } from './GenericEditableDateFieldEditMode';
export const GenericEditableDateField = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldDateMetadata>;
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
return (
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
<EditableField
IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditableDateFieldEditMode />}
displayModeContent={<DateDisplay value={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue}
/>
</RecoilScope>
);
};

View File

@ -1,76 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { DateInput } from '@/ui/input/components/DateInput';
import { Nullable } from '~/types/Nullable';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { useFieldInputEventHandlers } from '../hooks/useFieldInputEventHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldDateMetadata } from '../types/FieldMetadata';
export const GenericEditableDateFieldEditMode = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldDateMetadata>;
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const updateField = useUpdateGenericEntityField();
const handleSubmit = (newDate: Nullable<Date>) => {
if (!newDate) {
setFieldValue('');
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
'',
);
}
}
const newDateISO = newDate?.toISOString();
if (newDateISO === fieldValue || !newDateISO) return;
setFieldValue(newDateISO);
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
newDateISO,
);
}
};
const { handleEnter, handleEscape, handleClickOutside } =
useFieldInputEventHandlers({
onSubmit: handleSubmit,
});
return (
<DateInput
hotkeyScope={EditableFieldHotkeyScope.EditableField}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
value={fieldValue ? new Date(fieldValue) : null}
/>
);
};

View File

@ -1,47 +0,0 @@
import { useContext } from 'react';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldProbability } from '../types/guards/isFieldProbability';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldURL } from '../types/guards/isFieldURL';
import { GenericEditableBooleanField } from './GenericEditableBooleanField';
import { GenericEditableDateField } from './GenericEditableDateField';
import { GenericEditableNumberField } from './GenericEditableNumberField';
import { GenericEditablePhoneField } from './GenericEditablePhoneField';
import { GenericEditableRelationField } from './GenericEditableRelationField';
import { GenericEditableTextField } from './GenericEditableTextField';
import { GenericEditableURLField } from './GenericEditableURLField';
import { ProbabilityEditableField } from './ProbabilityEditableField';
export const GenericEditableField = () => {
const fieldDefinition = useContext(EditableFieldDefinitionContext);
if (isFieldRelation(fieldDefinition)) {
return <GenericEditableRelationField />;
} else if (isFieldDate(fieldDefinition)) {
return <GenericEditableDateField />;
} else if (isFieldNumber(fieldDefinition)) {
return <GenericEditableNumberField />;
} else if (isFieldProbability(fieldDefinition)) {
return <ProbabilityEditableField />;
} else if (isFieldURL(fieldDefinition)) {
return <GenericEditableURLField />;
} else if (isFieldText(fieldDefinition)) {
return <GenericEditableTextField />;
} else if (isFieldPhone(fieldDefinition)) {
return <GenericEditablePhoneField />;
} else if (isFieldBoolean(fieldDefinition)) {
return <GenericEditableBooleanField />;
} else {
console.warn(
`Unknown field metadata type: ${fieldDefinition.type} in GenericEditableField`,
);
return <></>;
}
};

View File

@ -1,42 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { NumberDisplay } from '@/ui/content-display/components/NumberDisplay';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { FieldRecoilScopeContext } from '../states/recoil-scope-contexts/FieldRecoilScopeContext';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldNumberMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField';
import { GenericEditableNumberFieldEditMode } from './GenericEditableNumberFieldEditMode';
export const GenericEditableNumberField = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldNumberMetadata>;
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
return (
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
<EditableField
IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditableNumberFieldEditMode />}
displayModeContent={<NumberDisplay value={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue}
/>
</RecoilScope>
);
};

View File

@ -1,73 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { TextInput } from '@/ui/input/components/TextInput';
import {
canBeCastAsIntegerOrNull,
castAsIntegerOrNull,
} from '~/utils/cast-as-integer-or-null';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { useFieldInputEventHandlers } from '../hooks/useFieldInputEventHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldNumberMetadata } from '../types/FieldMetadata';
export const GenericEditableNumberFieldEditMode = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldNumberMetadata>;
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<number | null>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const updateField = useUpdateGenericEntityField();
const handleSubmit = (newValue: string) => {
if (!canBeCastAsIntegerOrNull(newValue)) {
return;
}
if (newValue === fieldValue) return;
const castedValue = castAsIntegerOrNull(newValue);
setFieldValue(castedValue);
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
castedValue,
);
}
};
const { handleEnter, handleEscape, handleClickOutside } =
useFieldInputEventHandlers({
onSubmit: handleSubmit,
});
return (
<TextInput
autoFocus
placeholder={currentEditableFieldDefinition.metadata.placeHolder}
hotkeyScope={EditableFieldHotkeyScope.EditableField}
value={fieldValue ? fieldValue.toString() : ''}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
/>
);
};

View File

@ -1,43 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { PhoneDisplay } from '@/ui/content-display/components/PhoneDisplay';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { FieldRecoilScopeContext } from '../states/recoil-scope-contexts/FieldRecoilScopeContext';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldPhoneMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField';
import { GenericEditablePhoneFieldEditMode } from './GenericEditablePhoneFieldEditMode';
export const GenericEditablePhoneField = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldPhoneMetadata>;
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
return (
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
<EditableField
useEditButton
IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditablePhoneFieldEditMode />}
displayModeContent={<PhoneDisplay value={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue}
/>
</RecoilScope>
);
};

View File

@ -1,62 +0,0 @@
import { useContext } from 'react';
import { isPossiblePhoneNumber } from 'react-phone-number-input';
import { useRecoilState } from 'recoil';
import { PhoneInput } from '@/ui/input/components/PhoneInput';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { useFieldInputEventHandlers } from '../hooks/useFieldInputEventHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldPhoneMetadata } from '../types/FieldMetadata';
export const GenericEditablePhoneFieldEditMode = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldPhoneMetadata>;
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const updateField = useUpdateGenericEntityField();
const handleSubmit = (newValue: string) => {
if (!isPossiblePhoneNumber(newValue)) return;
setFieldValue(newValue);
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
newValue,
);
}
};
const { handleEnter, handleEscape, handleClickOutside } =
useFieldInputEventHandlers({
onSubmit: handleSubmit,
});
return (
<PhoneInput
value={fieldValue ?? ''}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
hotkeyScope={EditableFieldHotkeyScope.EditableField}
/>
);
};

View File

@ -1,50 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { FieldRecoilScopeContext } from '../states/recoil-scope-contexts/FieldRecoilScopeContext';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldRelationMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField';
import { GenericEditableRelationFieldDisplayMode } from './GenericEditableRelationFieldDisplayMode';
import { GenericEditableRelationFieldEditMode } from './GenericEditableRelationFieldEditMode';
export const GenericEditableRelationField = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldRelationMetadata>;
const fieldValue = useRecoilValue<any | null>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
return (
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
<RecoilScope>
<EditableField
useEditButton={currentEditableFieldDefinition.metadata.useEditButton}
customEditHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,
}}
IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditableRelationFieldEditMode />}
displayModeContent={<GenericEditableRelationFieldDisplayMode />}
isDisplayModeContentEmpty={!fieldValue}
isDisplayModeFixHeight
/>
</RecoilScope>
</RecoilScope>
);
};

View File

@ -1,70 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { CompanyChip } from '@/companies/components/CompanyChip';
import { PersonChip } from '@/people/components/PersonChip';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { UserChip } from '@/users/components/UserChip';
import { getLogoUrlFromDomainName } from '~/utils';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldRelationMetadata } from '../types/FieldMetadata';
export const GenericEditableRelationFieldDisplayMode = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldRelationMetadata>;
const fieldValue = useRecoilValue<any | null>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
switch (currentEditableFieldDefinition.metadata.relationType) {
case Entity.Person: {
return (
<PersonChip
id={fieldValue?.id ?? ''}
name={fieldValue?.displayName ?? ''}
pictureUrl={fieldValue?.avatarUrl ?? ''}
/>
);
}
case Entity.User: {
return (
<UserChip
id={fieldValue?.id ?? ''}
name={fieldValue?.displayName ?? ''}
pictureUrl={fieldValue?.avatarUrl ?? ''}
/>
);
}
case Entity.Company: {
return (
<CompanyChip
id={fieldValue?.id ?? ''}
name={fieldValue?.name ?? ''}
pictureUrl={
fieldValue?.domainName
? getLogoUrlFromDomainName(fieldValue.domainName)
: ''
}
/>
);
}
default:
console.warn(
`Unknown relation type: "${currentEditableFieldDefinition.metadata.relationType}"
in GenericEditableRelationField`,
);
return <> </>;
}
};

View File

@ -1,135 +0,0 @@
import { useContext } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { CompanyPicker } from '@/companies/components/CompanyPicker';
import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState';
import { PeoplePicker } from '@/people/components/PeoplePicker';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { UserPicker } from '@/users/components/UserPicker';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { useEditableField } from '../hooks/useEditableField';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import {
FieldRelationMetadata,
FieldRelationValue,
} from '../types/FieldMetadata';
const StyledRelationPickerContainer = styled.div`
left: 0px;
position: absolute;
top: -8px;
`;
const RelationPicker = ({
fieldDefinition,
fieldValue,
handleEntitySubmit,
handleCancel,
}: {
fieldDefinition: FieldDefinition<FieldRelationMetadata>;
fieldValue: FieldRelationValue & { companyId?: string };
handleEntitySubmit: (newRelationId: EntityForSelect | null) => void;
handleCancel: () => void;
}) => {
switch (fieldDefinition.metadata.relationType) {
case Entity.Person: {
return (
<PeoplePicker
personId={fieldValue ? fieldValue.id : ''}
companyId={fieldValue?.companyId ?? ''}
onSubmit={handleEntitySubmit}
onCancel={handleCancel}
/>
);
}
case Entity.User: {
return (
<UserPicker
userId={fieldValue ? fieldValue.id : ''}
onSubmit={handleEntitySubmit}
onCancel={handleCancel}
/>
);
}
case Entity.Company: {
return (
<CompanyPicker
companyId={fieldValue ? fieldValue.id : ''}
onSubmit={handleEntitySubmit}
onCancel={handleCancel}
/>
);
}
default:
console.warn(
`Unknown relation type: "${fieldDefinition.metadata.relationType}" in GenericEditableRelationField`,
);
return <></>;
}
};
export const GenericEditableRelationFieldEditMode = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldRelationMetadata>;
const [companyProgress] = useRecoilState(
companyProgressesFamilyState(currentEditableFieldEntityId ?? ''),
);
const { company } = companyProgress ?? {};
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<any | null>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const updateField = useUpdateGenericEntityField();
const { closeEditableField } = useEditableField();
const handleSubmit = (newRelation: EntityForSelect | null) => {
if (newRelation?.id === fieldValue?.id) return;
setFieldValue({
id: newRelation?.id ?? null,
displayName: newRelation?.name ?? null,
avatarUrl: newRelation?.avatarUrl ?? null,
});
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
newRelation,
);
}
closeEditableField();
};
const handleCancel = () => {
closeEditableField();
};
return (
<StyledRelationPickerContainer>
<RelationPicker
fieldDefinition={currentEditableFieldDefinition}
fieldValue={{ ...fieldValue, companyId: company?.id }}
handleEntitySubmit={handleSubmit}
handleCancel={handleCancel}
/>
</StyledRelationPickerContainer>
);
};

View File

@ -1,42 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { TextDisplay } from '@/ui/content-display/components/TextDisplay';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { FieldRecoilScopeContext } from '../states/recoil-scope-contexts/FieldRecoilScopeContext';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldNumberMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField';
import { GenericEditableTextFieldEditMode } from './GenericEditableTextFieldEditMode';
export const GenericEditableTextField = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldNumberMetadata>;
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
return (
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
<EditableField
IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditableTextFieldEditMode />}
displayModeContent={<TextDisplay text={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue}
/>
</RecoilScope>
);
};

View File

@ -1,62 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { TextInput } from '@/ui/input/components/TextInput';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { useFieldInputEventHandlers } from '../hooks/useFieldInputEventHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldTextMetadata } from '../types/FieldMetadata';
export const GenericEditableTextFieldEditMode = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldTextMetadata>;
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const updateField = useUpdateGenericEntityField();
const handleSubmit = (newValue: string) => {
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
newValue,
);
// TODO: use optimistic effect instead, but needs generic refactor
setFieldValue(newValue);
}
};
const { handleEnter, handleEscape, handleClickOutside } =
useFieldInputEventHandlers({
onSubmit: handleSubmit,
});
return (
<TextInput
placeholder={currentEditableFieldDefinition.metadata.placeHolder}
autoFocus
value={fieldValue ?? ''}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
hotkeyScope={EditableFieldHotkeyScope.EditableField}
/>
);
};

View File

@ -1,44 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { URLDisplay } from '@/ui/content-display/components/URLDisplay';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { FieldRecoilScopeContext } from '../states/recoil-scope-contexts/FieldRecoilScopeContext';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldNumberMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField';
import { GenericEditableURLFieldEditMode } from './GenericEditableURLFieldEditMode';
export const GenericEditableURLField = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldNumberMetadata>;
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
return (
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
<EditableField
useEditButton
IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditableURLFieldEditMode />}
displayModeContent={<URLDisplay value={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue}
isDisplayModeFixHeight
/>
</RecoilScope>
);
};

View File

@ -1,63 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { TextInput } from '@/ui/input/components/TextInput';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { useFieldInputEventHandlers } from '../hooks/useFieldInputEventHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldURLMetadata } from '../types/FieldMetadata';
// This one is very similar to GenericEditableTextFieldEditMode
// We could probably merge them since FieldURLMetadata is basically a FieldTextMetadata
export const GenericEditableURLFieldEditMode = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldURLMetadata>;
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const updateField = useUpdateGenericEntityField();
const handleSubmit = (newValue: string) => {
setFieldValue(newValue);
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
newValue,
);
}
};
const { handleEnter, handleEscape, handleClickOutside } =
useFieldInputEventHandlers({
onSubmit: handleSubmit,
});
return (
<TextInput
placeholder={currentEditableFieldDefinition.metadata.placeHolder}
autoFocus
value={fieldValue ?? ''}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
hotkeyScope={EditableFieldHotkeyScope.EditableField}
/>
);
};

View File

@ -0,0 +1,87 @@
import { useContext } from 'react';
import { FieldDisplay } from '@/ui/field/components/FieldDisplay';
import { FieldInput } from '@/ui/field/components/FieldInput';
import { FieldContext } from '@/ui/field/contexts/FieldContext';
import { useIsFieldEmpty } from '@/ui/field/hooks/useIsFieldEmpty';
import { useIsFieldInputOnly } from '@/ui/field/hooks/useIsFieldInputOnly';
import { FieldInputEvent } from '@/ui/field/types/FieldInputEvent';
import { isFieldRelation } from '@/ui/field/types/guards/isFieldRelation';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { useInlineCell } from '../hooks/useInlineCell';
import { InlineCellContainer } from './InlineCellContainer';
export const InlineCell = () => {
const { fieldDefinition } = useContext(FieldContext);
const isFieldEmpty = useIsFieldEmpty();
const isFieldInputOnly = useIsFieldInputOnly();
const { closeInlineCell } = useInlineCell();
const handleEnter: FieldInputEvent = (persistField) => {
persistField();
closeInlineCell();
};
const handleSubmit: FieldInputEvent = (persistField) => {
persistField();
closeInlineCell();
};
const handleCancel = () => {
closeInlineCell();
};
const handleEscape = () => {
closeInlineCell();
};
const handleTab: FieldInputEvent = (persistField) => {
persistField();
closeInlineCell();
};
const handleShiftTab: FieldInputEvent = (persistField) => {
persistField();
closeInlineCell();
};
const handleClickOutside: FieldInputEvent = (persistField) => {
persistField();
closeInlineCell();
};
console.log(JSON.stringify({ fieldDefinition }));
return (
<InlineCellContainer
useEditButton={fieldDefinition.useEditButton}
customEditHotkeyScope={
isFieldRelation(fieldDefinition)
? {
scope: RelationPickerHotkeyScope.RelationPicker,
}
: undefined
}
IconLabel={fieldDefinition.Icon}
editModeContent={
<FieldInput
onEnter={handleEnter}
onCancel={handleCancel}
onEscape={handleEscape}
onSubmit={handleSubmit}
onTab={handleTab}
onShiftTab={handleShiftTab}
onClickOutside={handleClickOutside}
/>
}
displayModeContent={<FieldDisplay />}
isDisplayModeContentEmpty={isFieldEmpty}
isDisplayModeFixHeight
editModeContentOnly={isFieldInputOnly}
/>
);
};

View File

@ -1,15 +1,16 @@
import { useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useEditableField } from '../hooks/useEditableField';
import { useInlineCell } from '../hooks/useInlineCell';
import { EditableFieldDisplayMode } from './EditableFieldDisplayMode';
import { EditableFieldEditButton } from './EditableFieldEditButton';
import { EditableFieldEditMode } from './EditableFieldEditMode';
import { InlineCellDisplayMode } from './InlineCellDisplayMode';
import { InlineCellEditButton } from './InlineCellEditButton';
import { InlineCellEditMode } from './InlineCellEditMode';
const StyledIconContainer = styled.div`
align-items: center;
@ -57,7 +58,7 @@ const StyledClickableContainer = styled.div`
width: 100%;
`;
const StyledEditableFieldBaseContainer = styled.div`
const StyledInlineCellBaseContainer = styled.div`
align-items: center;
box-sizing: border-box;
@ -77,7 +78,7 @@ type OwnProps = {
labelFixedWidth?: number;
useEditButton?: boolean;
editModeContent?: React.ReactNode;
displayModeContentOnly?: boolean;
editModeContentOnly?: boolean;
displayModeContent: React.ReactNode;
customEditHotkeyScope?: HotkeyScope;
isDisplayModeContentEmpty?: boolean;
@ -85,7 +86,7 @@ type OwnProps = {
disableHoverEffect?: boolean;
};
export const EditableField = ({
export const InlineCellContainer = ({
IconLabel,
label,
labelFixedWidth,
@ -94,7 +95,7 @@ export const EditableField = ({
displayModeContent,
customEditHotkeyScope,
isDisplayModeContentEmpty,
displayModeContentOnly,
editModeContentOnly,
isDisplayModeFixHeight,
disableHoverEffect,
}: OwnProps) => {
@ -108,46 +109,61 @@ export const EditableField = ({
setIsHovered(false);
};
const { isFieldInEditMode, openEditableField } = useEditableField();
const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
const handleDisplayModeClick = () => {
if (!displayModeContentOnly) {
openEditableField(customEditHotkeyScope);
if (!editModeContentOnly) {
openInlineCell(customEditHotkeyScope);
}
};
const showEditButton =
!isFieldInEditMode && isHovered && useEditButton && !displayModeContentOnly;
!isInlineCellInEditMode &&
isHovered &&
useEditButton &&
!editModeContentOnly;
const theme = useTheme();
return (
<StyledEditableFieldBaseContainer
<StyledInlineCellBaseContainer
onMouseEnter={handleContainerMouseEnter}
onMouseLeave={handleContainerMouseLeave}
>
<StyledLabelAndIconContainer>
{IconLabel && (
<StyledIconContainer>
<IconLabel />
<IconLabel stroke={theme.icon.stroke.sm} />
</StyledIconContainer>
)}
{label && (
<StyledLabel labelFixedWidth={labelFixedWidth}>{label}</StyledLabel>
)}
</StyledLabelAndIconContainer>
<StyledValueContainer>
{isFieldInEditMode ? (
<EditableFieldEditMode>{editModeContent}</EditableFieldEditMode>
{isInlineCellInEditMode ? (
<InlineCellEditMode>{editModeContent}</InlineCellEditMode>
) : editModeContentOnly ? (
<StyledClickableContainer>
<InlineCellDisplayMode
disableHoverEffect={disableHoverEffect}
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
isDisplayModeFixHeight={isDisplayModeFixHeight}
isHovered={isHovered}
>
{editModeContent}
</InlineCellDisplayMode>
</StyledClickableContainer>
) : (
<StyledClickableContainer onClick={handleDisplayModeClick}>
<EditableFieldDisplayMode
<InlineCellDisplayMode
disableHoverEffect={disableHoverEffect}
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
isDisplayModeFixHeight={isDisplayModeFixHeight}
isHovered={isHovered}
>
{displayModeContent}
</EditableFieldDisplayMode>
</InlineCellDisplayMode>
{showEditButton && (
<StyledEditButtonContainer
initial={{ opacity: 0 }}
@ -155,12 +171,12 @@ export const EditableField = ({
transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }}
>
<EditableFieldEditButton />
<InlineCellEditButton />
</StyledEditButtonContainer>
)}
</StyledClickableContainer>
)}
</StyledValueContainer>
</StyledEditableFieldBaseContainer>
</StyledInlineCellBaseContainer>
);
};

View File

@ -57,7 +57,7 @@ type OwnProps = {
isHovered?: boolean;
};
export const EditableFieldDisplayMode = ({
export const InlineCellDisplayMode = ({
children,
isDisplayModeContentEmpty,
disableHoverEffect,

View File

@ -1,13 +1,13 @@
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
import { IconPencil } from '@/ui/icon';
import { useEditableField } from '../hooks/useEditableField';
import { useInlineCell } from '../hooks/useInlineCell';
export const EditableFieldEditButton = () => {
const { openEditableField } = useEditableField();
export const InlineCellEditButton = () => {
const { openInlineCell } = useInlineCell();
const handleClick = () => {
openEditableField();
openInlineCell();
};
return (
@ -15,7 +15,7 @@ export const EditableFieldEditButton = () => {
size="small"
onClick={handleClick}
Icon={IconPencil}
data-testid="editable-field-edit-mode-container"
data-testid="inline-cell-edit-mode-container"
/>
);
};

View File

@ -1,6 +1,6 @@
import styled from '@emotion/styled';
const StyledEditableFieldEditModeContainer = styled.div<OwnProps>`
const StyledInlineCellEditModeContainer = styled.div<OwnProps>`
align-items: center;
display: flex;
@ -11,7 +11,7 @@ const StyledEditableFieldEditModeContainer = styled.div<OwnProps>`
z-index: 10;
`;
const StyledEditableFieldInput = styled.div`
const StyledInlineCellInput = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.transparent.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
@ -30,8 +30,8 @@ type OwnProps = {
children: React.ReactNode;
};
export const EditableFieldEditMode = ({ children }: OwnProps) => (
<StyledEditableFieldEditModeContainer data-testid="editable-field-edit-mode-container">
<StyledEditableFieldInput>{children}</StyledEditableFieldInput>
</StyledEditableFieldEditModeContainer>
export const InlineCellEditMode = ({ children }: OwnProps) => (
<StyledInlineCellEditModeContainer data-testid="inline-cell-edit-mode-container">
<StyledInlineCellInput>{children}</StyledInlineCellInput>
</StyledInlineCellEditModeContainer>
);

View File

@ -1,28 +0,0 @@
import { useContext } from 'react';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldProbabilityMetadata } from '../types/FieldMetadata';
import { ProbabilityEditableFieldEditMode } from './ProbabilityEditableFieldEditMode';
export const ProbabilityEditableField = () => {
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldProbabilityMetadata>;
return (
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
<EditableField
IconLabel={currentEditableFieldDefinition.Icon}
displayModeContent={<ProbabilityEditableFieldEditMode />}
displayModeContentOnly
disableHoverEffect
/>
</RecoilScope>
);
};

View File

@ -1,53 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { useEditableField } from '@/ui/editable-field/hooks/useEditableField';
import { ProbabilityInput } from '@/ui/input/components/ProbabilityInput';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldProbabilityMetadata } from '../types/FieldMetadata';
export const ProbabilityEditableFieldEditMode = () => {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldProbabilityMetadata>;
const [fieldValue, setFieldValue] = useRecoilState<number>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const { closeEditableField } = useEditableField();
const updateField = useUpdateGenericEntityField();
const probabilityIndex = Math.ceil(fieldValue / 25);
const handleChange = (newValue: number) => {
setFieldValue(newValue);
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
newValue,
);
}
closeEditableField();
};
return (
<ProbabilityInput
probabilityIndex={probabilityIndex}
onChange={handleChange}
/>
);
};

View File

@ -1,8 +0,0 @@
import { createContext } from 'react';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldMetadata } from '../types/FieldMetadata';
export const EditableFieldDefinitionContext = createContext<
FieldDefinition<FieldMetadata>
>({} as FieldDefinition<FieldMetadata>);

View File

@ -1,3 +0,0 @@
import { createContext } from 'react';
export const EditableFieldEntityIdContext = createContext<string>('');

View File

@ -1,28 +0,0 @@
import { useEditableField } from './useEditableField';
export const useFieldInputEventHandlers = <T>({
onSubmit,
onCancel,
}: {
onSubmit?: (newValue: T) => void;
onCancel?: () => void;
}) => {
const { closeEditableField, isFieldInEditMode } = useEditableField();
return {
handleClickOutside: (_event: MouseEvent | TouchEvent, newValue: T) => {
if (isFieldInEditMode) {
onSubmit?.(newValue);
closeEditableField();
}
},
handleEscape: () => {
closeEditableField();
onCancel?.();
},
handleEnter: (newValue: T) => {
onSubmit?.(newValue);
closeEditableField();
},
};
};

View File

@ -1,15 +1,18 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '@/ui/field/contexts/FieldContext';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { isFieldInEditModeScopedState } from '../states/isFieldInEditModeScopedState';
import { FieldRecoilScopeContext } from '../states/recoil-scope-contexts/FieldRecoilScopeContext';
import { isInlineCellInEditModeScopedState } from '../states/isInlineCellInEditModeScopedState';
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
export const useEditableField = () => {
const [isFieldInEditMode, setIsFieldInEditMode] = useRecoilScopedState(
isFieldInEditModeScopedState,
FieldRecoilScopeContext,
export const useInlineCell = () => {
const { recoilScopeId } = useContext(FieldContext);
const [isInlineCellInEditMode, setIsInlineCellInEditMode] = useRecoilState(
isInlineCellInEditModeScopedState(recoilScopeId),
);
const {
@ -17,14 +20,14 @@ export const useEditableField = () => {
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const closeEditableField = () => {
setIsFieldInEditMode(false);
const closeInlineCell = () => {
setIsInlineCellInEditMode(false);
goBackToPreviousHotkeyScope();
};
const openEditableField = (customEditHotkeyScopeForField?: HotkeyScope) => {
setIsFieldInEditMode(true);
const openInlineCell = (customEditHotkeyScopeForField?: HotkeyScope) => {
setIsInlineCellInEditMode(true);
if (customEditHotkeyScopeForField) {
setHotkeyScopeAndMemorizePreviousScope(
@ -39,8 +42,8 @@ export const useEditableField = () => {
};
return {
isFieldInEditMode,
closeEditableField,
openEditableField,
isInlineCellInEditMode,
closeInlineCell,
openInlineCell,
};
};

View File

@ -1,44 +0,0 @@
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
import { useEditableField } from './useEditableField';
export const useRegisterCloseFieldHandlers = (
wrapperRef: React.RefObject<HTMLDivElement>,
onSubmit?: () => void,
onCancel?: () => void,
) => {
const { closeEditableField, isFieldInEditMode } = useEditableField();
useListenClickOutside({
refs: [wrapperRef],
callback: () => {
if (isFieldInEditMode) {
onSubmit?.();
closeEditableField();
}
},
});
useScopedHotkeys(
'enter',
() => {
onSubmit?.();
closeEditableField();
},
EditableFieldHotkeyScope.EditableField,
[closeEditableField, onSubmit],
);
useScopedHotkeys(
'esc',
() => {
closeEditableField();
onCancel?.();
},
EditableFieldHotkeyScope.EditableField,
[closeEditableField, onCancel],
);
};

View File

@ -1,170 +0,0 @@
import { useContext } from 'react';
import { EditableFieldMutationContext } from '../contexts/EditableFieldMutationContext';
import { FieldDefinition } from '../types/FieldDefinition';
import {
FieldBooleanMetadata,
FieldBooleanValue,
FieldChipMetadata,
FieldChipValue,
FieldDateMetadata,
FieldDateValue,
FieldDoubleTextChipMetadata,
FieldDoubleTextChipValue,
FieldDoubleTextMetadata,
FieldDoubleTextValue,
FieldMetadata,
FieldNumberMetadata,
FieldNumberValue,
FieldPhoneMetadata,
FieldPhoneValue,
FieldProbabilityMetadata,
FieldProbabilityValue,
FieldRelationMetadata,
FieldRelationValue,
FieldTextMetadata,
FieldTextValue,
FieldURLMetadata,
FieldURLValue,
} from '../types/FieldMetadata';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
import { isFieldChip } from '../types/guards/isFieldChip';
import { isFieldChipValue } from '../types/guards/isFieldChipValue';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDateValue } from '../types/guards/isFieldDateValue';
import { isFieldDoubleText } from '../types/guards/isFieldDoubleText';
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
import { isFieldDoubleTextChipValue } from '../types/guards/isFieldDoubleTextChipValue';
import { isFieldDoubleTextValue } from '../types/guards/isFieldDoubleTextValue';
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 { isFieldProbability } from '../types/guards/isFieldProbability';
import { isFieldProbabilityValue } from '../types/guards/isFieldProbabilityValue';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldRelationValue } from '../types/guards/isFieldRelationValue';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldTextValue } from '../types/guards/isFieldTextValue';
import { isFieldURL } from '../types/guards/isFieldURL';
import { isFieldURLValue } from '../types/guards/isFieldURLValue';
export const useUpdateGenericEntityField = () => {
const useUpdateEntityMutation = useContext(EditableFieldMutationContext);
const [updateEntity] = useUpdateEntityMutation();
const updateEntityField = <
ValueType extends FieldMetadata extends FieldDoubleTextMetadata
? FieldDoubleTextValue
: FieldMetadata extends FieldTextMetadata
? FieldTextValue
: FieldMetadata extends FieldPhoneMetadata
? FieldPhoneValue
: FieldMetadata extends FieldURLMetadata
? FieldURLValue
: FieldMetadata extends FieldNumberMetadata
? FieldNumberValue
: FieldMetadata extends FieldDateMetadata
? FieldDateValue
: FieldMetadata extends FieldChipMetadata
? FieldChipValue
: FieldMetadata extends FieldDoubleTextChipMetadata
? FieldDoubleTextChipValue
: FieldMetadata extends FieldRelationMetadata
? FieldRelationValue
: FieldMetadata extends FieldProbabilityMetadata
? FieldProbabilityValue
: FieldMetadata extends FieldBooleanMetadata
? FieldBooleanValue
: unknown,
>(
currentEntityId: string,
field: FieldDefinition<FieldMetadata>,
newFieldValue: ValueType | null,
) => {
// TODO: improve type guards organization, maybe with a common typeguard for all fields
// taking an object of options as parameter ?
//
// The goal would be to check that the field value not only is valid,
// but also that it is validated against the corresponding field type
if (
// Relation
isFieldRelation(field) &&
isFieldRelationValue(newFieldValue)
) {
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[field.metadata.fieldName]: newFieldValue
? { connect: { id: newFieldValue.id } }
: { disconnect: true },
},
},
});
return;
}
if (
// Chip
isFieldChip(field) &&
isFieldChipValue(newFieldValue)
) {
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [field.metadata.contentFieldName]: newFieldValue },
},
});
return;
}
if (
// Text
(isFieldText(field) && isFieldTextValue(newFieldValue)) ||
// Phone
(isFieldPhone(field) && isFieldPhoneValue(newFieldValue)) ||
// URL
(isFieldURL(field) && isFieldURLValue(newFieldValue)) ||
// Number
(isFieldNumber(field) && isFieldNumberValue(newFieldValue)) ||
// Date
(isFieldDate(field) && isFieldDateValue(newFieldValue)) ||
// Probability
(isFieldProbability(field) && isFieldProbabilityValue(newFieldValue)) ||
// Boolean
(isFieldBoolean(field) && isFieldBooleanValue(newFieldValue))
) {
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [field.metadata.fieldName]: newFieldValue },
},
});
return;
}
if (
// Double text
(isFieldDoubleText(field) && isFieldDoubleTextValue(newFieldValue)) ||
// Double Text Chip
(isFieldDoubleTextChip(field) &&
isFieldDoubleTextChipValue(newFieldValue))
) {
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[field.metadata.firstValueFieldName]: newFieldValue.firstValue,
[field.metadata.secondValueFieldName]: newFieldValue.secondValue,
},
},
});
}
};
return updateEntityField;
};

View File

@ -1,9 +0,0 @@
import { atomFamily } from 'recoil';
export const genericEntitiesFamilyState = atomFamily<
Record<string, unknown> | null,
string
>({
key: 'genericEntitiesFamilyState',
default: null,
});

View File

@ -1,6 +0,0 @@
import { atomFamily } from 'recoil';
export const isFieldInEditModeScopedState = atomFamily<boolean, string>({
key: 'isFieldInEditModeScopedState',
default: false,
});

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const isInlineCellInEditModeScopedState = atomFamily<boolean, string>({
key: 'isInlineCellInEditModeScopedState',
default: false,
});

View File

@ -1,18 +0,0 @@
import { selectorFamily } from 'recoil';
import { genericEntitiesFamilyState } from '../genericEntitiesFamilyState';
export const genericEntityFieldFamilySelector = selectorFamily({
key: 'genericEntityFieldFamilySelector',
get:
<T>({ fieldName, entityId }: { fieldName: string; entityId: string }) =>
({ get }) =>
get(genericEntitiesFamilyState(entityId))?.[fieldName] as T,
set:
<T>({ fieldName, entityId }: { fieldName: string; entityId: string }) =>
({ set }, newValue: T) =>
set(genericEntitiesFamilyState(entityId), (prevState) => ({
...prevState,
[fieldName]: newValue,
})),
});

View File

@ -1,11 +0,0 @@
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { FieldMetadata, FieldType } from './FieldMetadata';
export type FieldDefinition<T extends FieldMetadata | unknown> = {
key: string;
name: string;
Icon?: IconComponent;
type: FieldType;
metadata: T;
};

View File

@ -1,150 +0,0 @@
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
export type ViewFieldType =
| 'text'
| 'relation'
| 'chip'
| 'double-text-chip'
| 'double-text'
| 'number'
| 'date'
| 'phone'
| 'email'
| 'url'
| 'probability'
| 'boolean'
| 'moneyAmount';
export type ViewFieldTextMetadata = {
type: 'text';
placeHolder: string;
fieldName: string;
};
export type ViewFieldPhoneMetadata = {
type: 'phone';
placeHolder: string;
fieldName: string;
};
export type ViewFieldEmailMetadata = {
type: 'email';
placeHolder: string;
fieldName: string;
};
export type ViewFieldURLMetadata = {
type: 'url';
placeHolder: string;
fieldName: string;
};
export type ViewFieldDateMetadata = {
type: 'date';
fieldName: string;
};
export type ViewFieldNumberMetadata = {
type: 'number';
fieldName: string;
isPositive?: boolean;
};
export type ViewFieldMoneyMetadata = {
type: 'moneyAmount';
fieldName: string;
};
export type ViewFieldBooleanMetadata = {
type: 'boolean';
fieldName: string;
};
export type ViewFieldRelationMetadata = {
type: 'relation';
relationType: Entity;
fieldName: string;
useEditButton?: boolean;
};
export type ViewFieldChipMetadata = {
type: 'chip';
relationType: Entity;
contentFieldName: string;
urlFieldName: string;
placeHolder: string;
};
export type ViewFieldDoubleTextMetadata = {
type: 'double-text';
firstValueFieldName: string;
firstValuePlaceholder: string;
secondValueFieldName: string;
secondValuePlaceholder: string;
};
export type ViewFieldDoubleTextChipMetadata = {
type: 'double-text-chip';
firstValueFieldName: string;
firstValuePlaceholder: string;
secondValueFieldName: string;
secondValuePlaceholder: string;
avatarUrlFieldName: string;
entityType: Entity;
};
export type ViewFieldProbabilityMetadata = {
type: 'probability';
fieldName: string;
};
export type ViewFieldMetadata = { type: ViewFieldType } & (
| ViewFieldTextMetadata
| ViewFieldRelationMetadata
| ViewFieldChipMetadata
| ViewFieldDoubleTextChipMetadata
| ViewFieldDoubleTextMetadata
| ViewFieldPhoneMetadata
| ViewFieldEmailMetadata
| ViewFieldURLMetadata
| ViewFieldNumberMetadata
| ViewFieldBooleanMetadata
| ViewFieldDateMetadata
| ViewFieldProbabilityMetadata
| ViewFieldMoneyMetadata
);
export type ViewFieldDefinition<T extends ViewFieldMetadata | unknown> = {
Icon?: IconComponent;
index: number;
isVisible?: boolean;
key: string;
metadata: T;
name: string;
};
export type ViewFieldTextValue = string;
export type ViewFieldChipValue = string;
export type ViewFieldDateValue = string;
export type ViewFieldPhoneValue = string;
export type ViewFieldEmailValue = string;
export type ViewFieldBooleanValue = boolean;
export type ViewFieldMoneyValue = number | null;
export type ViewFieldURLValue = string;
export type ViewFieldNumberValue = number | null;
export type ViewFieldProbabilityValue = number;
export type ViewFieldDoubleTextValue = {
firstValue: string;
secondValue: string;
};
export type ViewFieldDoubleTextChipValue = {
firstValue: string;
secondValue: string;
};
export type ViewFieldRelationValue = EntityForSelect | null;

View File

@ -1,10 +0,0 @@
import {
ViewFieldBooleanMetadata,
ViewFieldDefinition,
ViewFieldMetadata,
} from '../ViewField';
export const isViewFieldBoolean = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldBooleanMetadata> =>
field.metadata.type === 'boolean';

View File

@ -1,5 +0,0 @@
import { ViewFieldBooleanValue } from '../ViewField';
export const isViewFieldBooleanValue = (
fieldValue: unknown,
): fieldValue is ViewFieldBooleanValue => typeof fieldValue === 'boolean';

View File

@ -1,10 +0,0 @@
import {
ViewFieldChipMetadata,
ViewFieldDefinition,
ViewFieldMetadata,
} from '../ViewField';
export const isViewFieldChip = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldChipMetadata> =>
field.metadata.type === 'chip';

View File

@ -1,9 +0,0 @@
import { ViewFieldChipValue } from '../ViewField';
// TODO: add yup
export const isViewFieldChipValue = (
fieldValue: unknown,
): fieldValue is ViewFieldChipValue =>
fieldValue !== null &&
fieldValue !== undefined &&
typeof fieldValue === 'string';

View File

@ -1,10 +0,0 @@
import {
ViewFieldDateMetadata,
ViewFieldDefinition,
ViewFieldMetadata,
} from '../ViewField';
export const isViewFieldDate = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldDateMetadata> =>
field.metadata.type === 'date';

View File

@ -1,9 +0,0 @@
import { ViewFieldDateValue } from '../ViewField';
// TODO: add yup
export const isViewFieldDateValue = (
fieldValue: unknown,
): fieldValue is ViewFieldDateValue =>
fieldValue !== null &&
fieldValue !== undefined &&
typeof fieldValue === 'string';

View File

@ -1,10 +0,0 @@
import {
ViewFieldDefinition,
ViewFieldDoubleTextMetadata,
ViewFieldMetadata,
} from '../ViewField';
export const isViewFieldDoubleText = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldDoubleTextMetadata> =>
field.metadata.type === 'double-text';

View File

@ -1,10 +0,0 @@
import {
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
ViewFieldMetadata,
} from '../ViewField';
export const isViewFieldDoubleTextChip = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldDoubleTextChipMetadata> =>
field.metadata.type === 'double-text-chip';

View File

@ -1,9 +0,0 @@
import { ViewFieldDoubleTextChipValue } from '../ViewField';
// TODO: add yup
export const isViewFieldDoubleTextChipValue = (
fieldValue: unknown,
): fieldValue is ViewFieldDoubleTextChipValue =>
fieldValue !== null &&
fieldValue !== undefined &&
typeof fieldValue === 'object';

View File

@ -1,9 +0,0 @@
import { ViewFieldDoubleTextValue } from '../ViewField';
// TODO: add yup
export const isViewFieldDoubleTextValue = (
fieldValue: unknown,
): fieldValue is ViewFieldDoubleTextValue =>
fieldValue !== null &&
fieldValue !== undefined &&
typeof fieldValue === 'object';

View File

@ -1,10 +0,0 @@
import {
ViewFieldDefinition,
ViewFieldEmailMetadata,
ViewFieldMetadata,
} from '../ViewField';
export const isViewFieldEmail = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldEmailMetadata> =>
field.metadata.type === 'email';

View File

@ -1,8 +0,0 @@
import { ViewFieldEmailValue } from '../ViewField';
export const isViewFieldEmailValue = (
fieldValue: unknown,
): fieldValue is ViewFieldEmailValue =>
fieldValue !== null &&
fieldValue !== undefined &&
typeof fieldValue === 'string';

View File

@ -1,10 +0,0 @@
import {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldMoneyMetadata,
} from '../ViewField';
export const isViewFieldMoney = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldMoneyMetadata> =>
field.metadata.type === 'moneyAmount';

View File

@ -1,7 +0,0 @@
import { ViewFieldMoneyValue } from '../ViewField';
export const isViewFieldMoneyValue = (
fieldValue: unknown,
): fieldValue is ViewFieldMoneyValue =>
fieldValue === null ||
(fieldValue !== undefined && typeof fieldValue === 'number');

View File

@ -1,10 +0,0 @@
import {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldNumberMetadata,
} from '../ViewField';
export const isViewFieldNumber = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldNumberMetadata> =>
field.metadata.type === 'number';

View File

@ -1,9 +0,0 @@
import { ViewFieldNumberValue } from '../ViewField';
// TODO: add yup
export const isViewFieldNumberValue = (
fieldValue: unknown,
): fieldValue is ViewFieldNumberValue =>
fieldValue !== null &&
fieldValue !== undefined &&
typeof fieldValue === 'number';

View File

@ -1,10 +0,0 @@
import {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldPhoneMetadata,
} from '../ViewField';
export const isViewFieldPhone = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldPhoneMetadata> =>
field.metadata.type === 'phone';

View File

@ -1,9 +0,0 @@
import { ViewFieldPhoneValue } from '../ViewField';
// TODO: add yup
export const isViewFieldPhoneValue = (
fieldValue: unknown,
): fieldValue is ViewFieldPhoneValue =>
fieldValue !== null &&
fieldValue !== undefined &&
typeof fieldValue === 'string';

View File

@ -1,10 +0,0 @@
import {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldProbabilityMetadata,
} from '../ViewField';
export const isViewFieldProbability = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldProbabilityMetadata> =>
field.metadata.type === 'probability';

View File

@ -1,9 +0,0 @@
import { ViewFieldProbabilityValue } from '../ViewField';
// TODO: add yup
export const isViewFieldProbabilityValue = (
fieldValue: unknown,
): fieldValue is ViewFieldProbabilityValue =>
fieldValue !== null &&
fieldValue !== undefined &&
typeof fieldValue === 'number';

View File

@ -1,10 +0,0 @@
import {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldRelationMetadata,
} from '../ViewField';
export const isViewFieldRelation = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldRelationMetadata> =>
field.metadata.type === 'relation';

View File

@ -1,7 +0,0 @@
import { ViewFieldRelationValue } from '../ViewField';
// TODO: add yup
export const isViewFieldRelationValue = (
fieldValue: unknown,
): fieldValue is ViewFieldRelationValue =>
fieldValue !== undefined && typeof fieldValue === 'object';

View File

@ -1,10 +0,0 @@
import {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldTextMetadata,
} from '../ViewField';
export const isViewFieldText = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldTextMetadata> =>
field.metadata.type === 'text';

View File

@ -1,9 +0,0 @@
import { ViewFieldTextValue } from '../ViewField';
// TODO: add yup
export const isViewFieldTextValue = (
fieldValue: unknown,
): fieldValue is ViewFieldTextValue =>
fieldValue !== null &&
fieldValue !== undefined &&
typeof fieldValue === 'string';

View File

@ -1,10 +0,0 @@
import {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldURLMetadata,
} from '../ViewField';
export const isViewFieldURL = (
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldURLMetadata> =>
field.metadata.type === 'url';

View File

@ -1,6 +1,6 @@
import { DateDisplay } from '@/ui/content-display/components/DateDisplay';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { InlineCellContainer } from '@/ui/editable-field/components/InlineCellContainer';
import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
import { DateDisplay } from '@/ui/field/meta-types/display/content-display/components/DateDisplay';
import { IconComponent } from '@/ui/icon/types/IconComponent';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { parseDate } from '~/utils/date-utils';
@ -30,7 +30,7 @@ export const DateEditableField = ({
return (
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
<EditableField
<InlineCellContainer
IconLabel={Icon}
label={label}
editModeContent={

View File

@ -4,7 +4,7 @@ import { DateInput } from '@/ui/input/components/DateInput';
import { Nullable } from '~/types/Nullable';
import { parseDate } from '~/utils/date-utils';
import { useEditableField } from '../../hooks/useEditableField';
import { useInlineCell } from '../../hooks/useInlineCell';
type OwnProps = {
value: string;
@ -24,7 +24,7 @@ export const EditableFieldEditModeDate = ({
setInternalValue(value);
}, [value]);
const { closeEditableField } = useEditableField();
const { closeInlineCell: closeEditableField } = useInlineCell();
const handleClickOutside = () => {
closeEditableField();

View File

@ -0,0 +1,59 @@
import { useContext } from 'react';
import { FieldContext } from '../contexts/FieldContext';
import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisplay';
import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
import { DoubleTextChipFieldDisplay } from '../meta-types/display/components/DoubleTextChipFieldDisplay';
import { DoubleTextFieldDisplay } from '../meta-types/display/components/DoubleTextFieldDisplay';
import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay';
import { MoneyFieldDisplay } from '../meta-types/display/components/MoneyFieldDisplay';
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 { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay';
import { URLFieldDisplay } from '../meta-types/display/components/URLFieldDisplay';
import { isFieldChip } from '../types/guards/isFieldChip';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDoubleText } from '../types/guards/isFieldDoubleText';
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldMoney } from '../types/guards/isFieldMoney';
import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldURL } from '../types/guards/isFieldURL';
export const FieldDisplay = () => {
const { fieldDefinition } = useContext(FieldContext);
return (
<>
{isFieldRelation(fieldDefinition) ? (
<RelationFieldDisplay />
) : isFieldText(fieldDefinition) ? (
<TextFieldDisplay />
) : isFieldEmail(fieldDefinition) ? (
<EmailFieldDisplay />
) : isFieldDate(fieldDefinition) ? (
<DateFieldDisplay />
) : isFieldNumber(fieldDefinition) ? (
<NumberFieldDisplay />
) : isFieldMoney(fieldDefinition) ? (
<MoneyFieldDisplay />
) : isFieldURL(fieldDefinition) ? (
<URLFieldDisplay />
) : isFieldPhone(fieldDefinition) ? (
<PhoneFieldDisplay />
) : isFieldChip(fieldDefinition) ? (
<ChipFieldDisplay />
) : isFieldDoubleTextChip(fieldDefinition) ? (
<DoubleTextChipFieldDisplay />
) : isFieldDoubleText(fieldDefinition) ? (
<DoubleTextFieldDisplay />
) : (
<></>
)}
</>
);
};

View File

@ -0,0 +1,150 @@
import { useContext } from 'react';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { FieldContext } from '../contexts/FieldContext';
import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput';
import { ChipFieldInput } from '../meta-types/input/components/ChipFieldInput';
import { DateFieldInput } from '../meta-types/input/components/DateFieldInput';
import { DoubleTextChipFieldInput } from '../meta-types/input/components/DoubleTextChipFieldInput';
import { DoubleTextFieldInput } from '../meta-types/input/components/DoubleTextFieldInput';
import { EmailFieldInput } from '../meta-types/input/components/EmailFieldInput';
import { MoneyFieldInput } from '../meta-types/input/components/MoneyFieldInput';
import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInput';
import { PhoneFieldInput } from '../meta-types/input/components/PhoneFieldInput';
import { ProbabilityFieldInput } from '../meta-types/input/components/ProbabilityFieldInput';
import { RelationFieldInput } from '../meta-types/input/components/RelationFieldInput';
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
import { URLFieldInput } from '../meta-types/input/components/URLFieldInput';
import { FieldInputEvent } from '../types/FieldInputEvent';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldChip } from '../types/guards/isFieldChip';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDoubleText } from '../types/guards/isFieldDoubleText';
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldMoney } from '../types/guards/isFieldMoney';
import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldProbability } from '../types/guards/isFieldProbability';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldURL } from '../types/guards/isFieldURL';
type OwnProps = {
onSubmit?: FieldInputEvent;
onCancel?: () => void;
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const FieldInput = ({
onCancel,
onSubmit,
onEnter,
onEscape,
onShiftTab,
onTab,
onClickOutside,
}: OwnProps) => {
const { fieldDefinition } = useContext(FieldContext);
return (
<>
{isFieldRelation(fieldDefinition) ? (
<RecoilScope>
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
</RecoilScope>
) : 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}
/>
) : isFieldDate(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}
/>
) : isFieldURL(fieldDefinition) ? (
<URLFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldPhone(fieldDefinition) ? (
<PhoneFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldBoolean(fieldDefinition) ? (
<BooleanFieldInput onSubmit={onSubmit} />
) : isFieldProbability(fieldDefinition) ? (
<ProbabilityFieldInput onSubmit={onSubmit} />
) : isFieldChip(fieldDefinition) ? (
<ChipFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldDoubleTextChip(fieldDefinition) ? (
<DoubleTextChipFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldDoubleText(fieldDefinition) ? (
<DoubleTextFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldMoney(fieldDefinition) ? (
<MoneyFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : (
<></>
)}
</>
);
};

View File

@ -0,0 +1,17 @@
import { createContext } from 'react';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldMetadata } from '../types/FieldMetadata';
type GenericFieldContextType = {
fieldDefinition: FieldDefinition<FieldMetadata>;
// TODO: add better typing for mutation hook
useUpdateEntityMutation: () => [(params: any) => void, any];
entityId: string;
recoilScopeId: string;
hotkeyScope: string;
};
export const FieldContext = createContext<GenericFieldContextType>(
{} as GenericFieldContextType,
);

View File

@ -0,0 +1,23 @@
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: {
key: fieldDefinition.key,
name: fieldDefinition.name,
type: fieldDefinition.type,
metadata: fieldDefinition.metadata,
},
entityId,
}),
);
return isFieldEmpty;
};

View File

@ -0,0 +1,15 @@
import { useContext } from 'react';
import { FieldContext } from '../contexts/FieldContext';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldProbability } from '../types/guards/isFieldProbability';
export const useIsFieldInputOnly = () => {
const { fieldDefinition } = useContext(FieldContext);
if (isFieldBoolean(fieldDefinition) || isFieldProbability(fieldDefinition)) {
return true;
}
return false;
};

View File

@ -0,0 +1,184 @@
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';
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
import { isFieldChip } from '../types/guards/isFieldChip';
import { isFieldChipValue } from '../types/guards/isFieldChipValue';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDateValue } from '../types/guards/isFieldDateValue';
import { isFieldDoubleText } from '../types/guards/isFieldDoubleText';
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
import { isFieldDoubleTextChipValue } from '../types/guards/isFieldDoubleTextChipValue';
import { isFieldDoubleTextValue } from '../types/guards/isFieldDoubleTextValue';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldEmailValue } from '../types/guards/isFieldEmailValue';
import { isFieldMoney } from '../types/guards/isFieldMoney';
import { isFieldMoneyValue } from '../types/guards/isFieldMoneyValue';
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 { isFieldProbability } from '../types/guards/isFieldProbability';
import { isFieldProbabilityValue } from '../types/guards/isFieldProbabilityValue';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldRelationValue } from '../types/guards/isFieldRelationValue';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldTextValue } from '../types/guards/isFieldTextValue';
import { isFieldURL } from '../types/guards/isFieldURL';
import { isFieldURLValue } from '../types/guards/isFieldURLValue';
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 fieldIsChip =
isFieldChip(fieldDefinition) && isFieldChipValue(valueToPersist);
const fieldIsDoubleText =
isFieldDoubleText(fieldDefinition) &&
isFieldDoubleTextValue(valueToPersist);
const fieldIsDoubleTextChip =
isFieldDoubleTextChip(fieldDefinition) &&
isFieldDoubleTextChipValue(valueToPersist);
const fieldIsText =
isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist);
const fieldIsEmail =
isFieldEmail(fieldDefinition) && isFieldEmailValue(valueToPersist);
const fieldIsDate =
isFieldDate(fieldDefinition) && isFieldDateValue(valueToPersist);
const fieldIsURL =
isFieldURL(fieldDefinition) && isFieldURLValue(valueToPersist);
const fieldIsBoolean =
isFieldBoolean(fieldDefinition) &&
isFieldBooleanValue(valueToPersist);
const fieldIsProbability =
isFieldProbability(fieldDefinition) &&
isFieldProbabilityValue(valueToPersist);
const fieldIsNumber =
isFieldNumber(fieldDefinition) && isFieldNumberValue(valueToPersist);
const fieldIsMoney =
isFieldMoney(fieldDefinition) && isFieldMoneyValue(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: {
[fieldName]: valueToPersist
? { connect: { id: valueToPersist.id } }
: { disconnect: true },
},
},
});
} else if (fieldIsChip) {
const fieldName = fieldDefinition.metadata.contentFieldName;
set(
entityFieldsFamilySelector({ entityId, fieldName }),
valueToPersist,
);
updateEntity({
variables: {
where: { id: entityId },
data: {
[fieldName]: valueToPersist,
},
},
});
} else if (fieldIsDoubleText || fieldIsDoubleTextChip) {
set(
entityFieldsFamilySelector({
entityId,
fieldName: fieldDefinition.metadata.firstValueFieldName,
}),
valueToPersist.firstValue,
);
set(
entityFieldsFamilySelector({
entityId,
fieldName: fieldDefinition.metadata.secondValueFieldName,
}),
valueToPersist.secondValue,
);
updateEntity({
variables: {
where: { id: entityId },
data: {
[fieldDefinition.metadata.firstValueFieldName]:
valueToPersist.firstValue,
[fieldDefinition.metadata.secondValueFieldName]:
valueToPersist.secondValue,
},
},
});
} else if (
fieldIsText ||
fieldIsBoolean ||
fieldIsURL ||
fieldIsEmail ||
fieldIsProbability ||
fieldIsNumber ||
fieldIsMoney ||
fieldIsDate ||
fieldIsPhone
) {
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: ${valueToPersist} for type : ${fieldDefinition.type}, type may not be implemented in usePersistField.`,
);
}
},
[entityId, fieldDefinition, updateEntity],
);
return persistField;
};

View File

@ -0,0 +1,16 @@
import { useChipField } from '../../hooks/useChipField';
import { ChipDisplay } from '../content-display/components/ChipDisplay';
export const ChipFieldDisplay = () => {
const { avatarFieldValue, contentFieldValue, entityType, entityId } =
useChipField();
return (
<ChipDisplay
displayName={contentFieldValue}
avatarUrlValue={avatarFieldValue}
entityType={entityType}
entityId={entityId}
/>
);
};

View File

@ -0,0 +1,9 @@
import { DateDisplay } from '@/ui/field/meta-types/display/content-display/components/DateDisplay';
import { useDateField } from '../../hooks/useDateField';
export const DateFieldDisplay = () => {
const { fieldValue } = useDateField();
return <DateDisplay value={fieldValue} />;
};

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