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:
@ -61,7 +61,8 @@
|
|||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"xlsx-ugnis": "^0.19.3",
|
"xlsx-ugnis": "^0.19.3",
|
||||||
"yup": "^1.2.0"
|
"yup": "^1.2.0",
|
||||||
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "PORT=3001 craco start --max-warnings=0",
|
"start": "PORT=3001 craco start --max-warnings=0",
|
||||||
|
|||||||
@ -58,6 +58,7 @@ export const ActivityAssigneePicker = ({
|
|||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
avatarType: 'rounded',
|
avatarType: 'rounded',
|
||||||
avatarUrl: user.avatarUrl ?? '',
|
avatarUrl: user.avatarUrl ?? '',
|
||||||
|
originalEntity: user,
|
||||||
}),
|
}),
|
||||||
selectedIds: activity?.accountOwner?.id ? [activity?.accountOwner?.id] : [],
|
selectedIds: activity?.accountOwner?.id ? [activity?.accountOwner?.id] : [],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,9 +8,7 @@ import { ActivityComments } from '@/activities/components/ActivityComments';
|
|||||||
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
|
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
|
||||||
import { GET_ACTIVITIES } from '@/activities/graphql/queries/getActivities';
|
import { GET_ACTIVITIES } from '@/activities/graphql/queries/getActivities';
|
||||||
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
|
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
|
||||||
import { EditableFieldHotkeyScope } from '@/ui/editable-field/types/EditableFieldHotkeyScope';
|
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||||
import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField';
|
|
||||||
import { IconCalendar } from '@/ui/icon/index';
|
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
@ -22,6 +20,7 @@ import {
|
|||||||
import { debounce } from '~/utils/debounce';
|
import { debounce } from '~/utils/debounce';
|
||||||
|
|
||||||
import { ActivityAssigneeEditableField } from '../editable-fields/components/ActivityAssigneeEditableField';
|
import { ActivityAssigneeEditableField } from '../editable-fields/components/ActivityAssigneeEditableField';
|
||||||
|
import { ActivityEditorDateField } from '../editable-fields/components/ActivityEditorDateField';
|
||||||
import { ActivityRelationEditableField } from '../editable-fields/components/ActivityRelationEditableField';
|
import { ActivityRelationEditableField } from '../editable-fields/components/ActivityRelationEditableField';
|
||||||
import { ACTIVITY_UPDATE_FRAGMENT } from '../graphql/fragments/activityUpdateFragment';
|
import { ACTIVITY_UPDATE_FRAGMENT } from '../graphql/fragments/activityUpdateFragment';
|
||||||
import { CommentForDrawer } from '../types/CommentForDrawer';
|
import { CommentForDrawer } from '../types/CommentForDrawer';
|
||||||
@ -185,26 +184,12 @@ export const ActivityEditor = ({
|
|||||||
<PropertyBox>
|
<PropertyBox>
|
||||||
{activity.type === ActivityType.Task && (
|
{activity.type === ActivityType.Task && (
|
||||||
<>
|
<>
|
||||||
<DateEditableField
|
<RecoilScope>
|
||||||
value={activity.dueAt}
|
<ActivityEditorDateField activityId={activity.id} />
|
||||||
Icon={IconCalendar}
|
</RecoilScope>
|
||||||
label="Due date"
|
<RecoilScope>
|
||||||
onSubmit={(newDate) => {
|
<ActivityAssigneeEditableField activity={activity} />
|
||||||
updateActivityMutation({
|
</RecoilScope>
|
||||||
variables: {
|
|
||||||
where: {
|
|
||||||
id: activity.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
dueAt: newDate,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
refetchQueries: [getOperationName(GET_ACTIVITIES) ?? ''],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
hotkeyScope={EditableFieldHotkeyScope.EditableField}
|
|
||||||
/>
|
|
||||||
<ActivityAssigneeEditableField activity={activity} />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<ActivityRelationEditableField activity={activity} />
|
<ActivityRelationEditableField activity={activity} />
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { EditableField } from '@/ui/editable-field/components/EditableField';
|
import { InlineCell } from '@/ui/editable-field/components/InlineCell';
|
||||||
import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
|
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 { IconUserCircle } from '@/ui/icon';
|
||||||
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
|
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
||||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
import { Company, User, useUpdateActivityMutation } from '~/generated/graphql';
|
||||||
import { UserChip } from '@/users/components/UserChip';
|
|
||||||
import { Company, User } from '~/generated/graphql';
|
|
||||||
|
|
||||||
import { ActivityAssigneeEditableFieldEditMode } from './ActivityAssigneeEditableFieldEditMode';
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
activity: Pick<Company, 'id' | 'accountOwnerId'> & {
|
activity: Pick<Company, 'id' | 'accountOwnerId'> & {
|
||||||
@ -16,32 +15,25 @@ type OwnProps = {
|
|||||||
|
|
||||||
export const ActivityAssigneeEditableField = ({ activity }: OwnProps) => {
|
export const ActivityAssigneeEditableField = ({ activity }: OwnProps) => {
|
||||||
return (
|
return (
|
||||||
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
|
<FieldContext.Provider
|
||||||
<RecoilScope>
|
value={{
|
||||||
<EditableField
|
entityId: activity.id,
|
||||||
customEditHotkeyScope={{
|
recoilScopeId: 'assignee',
|
||||||
scope: RelationPickerHotkeyScope.RelationPicker,
|
fieldDefinition: {
|
||||||
}}
|
key: 'assignee',
|
||||||
label="Assignee"
|
name: 'Assignee',
|
||||||
IconLabel={IconUserCircle}
|
Icon: IconUserCircle,
|
||||||
editModeContent={
|
type: 'relation',
|
||||||
<ActivityAssigneeEditableFieldEditMode activity={activity} />
|
metadata: {
|
||||||
}
|
fieldName: 'assignee',
|
||||||
displayModeContent={
|
relationType: Entity.User,
|
||||||
activity.assignee?.displayName ? (
|
},
|
||||||
<UserChip
|
} satisfies FieldDefinition<FieldRelationMetadata>,
|
||||||
id={activity.assignee.id}
|
useUpdateEntityMutation: useUpdateActivityMutation,
|
||||||
name={activity.assignee?.displayName ?? ''}
|
hotkeyScope: EditableFieldHotkeyScope.EditableField,
|
||||||
pictureUrl={activity.assignee?.avatarUrl ?? ''}
|
}}
|
||||||
/>
|
>
|
||||||
) : (
|
<InlineCell />
|
||||||
<></>
|
</FieldContext.Provider>
|
||||||
)
|
|
||||||
}
|
|
||||||
isDisplayModeContentEmpty={!activity.assignee}
|
|
||||||
isDisplayModeFixHeight={true}
|
|
||||||
/>
|
|
||||||
</RecoilScope>
|
|
||||||
</RecoilScope>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { ActivityAssigneePicker } from '@/activities/components/ActivityAssigneePicker';
|
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';
|
import { Activity, User } from '~/generated/graphql';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -23,7 +23,7 @@ export const ActivityAssigneeEditableFieldEditMode = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
const { closeEditableField } = useEditableField();
|
const { closeInlineCell: closeEditableField } = useInlineCell();
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
closeEditableField();
|
closeEditableField();
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
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 { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
|
||||||
import { IconArrowUpRight } from '@/ui/icon';
|
import { IconArrowUpRight } from '@/ui/icon';
|
||||||
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
|
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
|
||||||
@ -23,7 +23,7 @@ export const ActivityRelationEditableField = ({ activity }: OwnProps) => {
|
|||||||
return (
|
return (
|
||||||
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
|
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
|
||||||
<RecoilScope>
|
<RecoilScope>
|
||||||
<EditableField
|
<InlineCellContainer
|
||||||
useEditButton
|
useEditButton
|
||||||
customEditHotkeyScope={{
|
customEditHotkeyScope={{
|
||||||
scope: RelationPickerHotkeyScope.RelationPicker,
|
scope: RelationPickerHotkeyScope.RelationPicker,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useHandleCheckableActivityTargetChange } from '@/activities/hooks/useHa
|
|||||||
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/activities/utils/flatMapAndSortEntityForSelectArrayByName';
|
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/activities/utils/flatMapAndSortEntityForSelectArrayByName';
|
||||||
import { useFilteredSearchCompanyQuery } from '@/companies/hooks/useFilteredSearchCompanyQuery';
|
import { useFilteredSearchCompanyQuery } from '@/companies/hooks/useFilteredSearchCompanyQuery';
|
||||||
import { useFilteredSearchPeopleQuery } from '@/people/hooks/useFilteredSearchPeopleQuery';
|
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 { MultipleEntitySelect } from '@/ui/input/relation-picker/components/MultipleEntitySelect';
|
||||||
import { Activity, ActivityTarget } from '~/generated/graphql';
|
import { Activity, ActivityTarget } from '~/generated/graphql';
|
||||||
import { assertNotNull } from '~/utils/assert';
|
import { assertNotNull } from '~/utils/assert';
|
||||||
@ -88,7 +88,7 @@ export const ActivityRelationEditableFieldEditMode = ({
|
|||||||
const handleCheckItemsChange = useHandleCheckableActivityTargetChange({
|
const handleCheckItemsChange = useHandleCheckableActivityTargetChange({
|
||||||
activity,
|
activity,
|
||||||
});
|
});
|
||||||
const { closeEditableField } = useEditableField();
|
const { closeInlineCell: closeEditableField } = useInlineCell();
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
handleCheckItemsChange(selectedEntityIds, entitiesToSelect);
|
handleCheckItemsChange(selectedEntityIds, entitiesToSelect);
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { ActivityEditor } from '@/activities/components/ActivityEditor';
|
import { ActivityEditor } from '@/activities/components/ActivityEditor';
|
||||||
|
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
|
||||||
import { useGetActivityQuery } from '~/generated/graphql';
|
import { useGetActivityQuery } from '~/generated/graphql';
|
||||||
|
|
||||||
import '@blocknote/core/style.css';
|
import '@blocknote/core/style.css';
|
||||||
@ -27,12 +29,20 @@ export const RightDrawerActivity = ({
|
|||||||
showComment = true,
|
showComment = true,
|
||||||
autoFillTitle = false,
|
autoFillTitle = false,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
|
const [, setEntityFields] = useRecoilState(
|
||||||
|
entityFieldsFamilyState(activityId),
|
||||||
|
);
|
||||||
|
|
||||||
const { data } = useGetActivityQuery({
|
const { data } = useGetActivityQuery({
|
||||||
variables: {
|
variables: {
|
||||||
activityId: activityId ?? '',
|
activityId: activityId ?? '',
|
||||||
},
|
},
|
||||||
skip: !activityId,
|
skip: !activityId,
|
||||||
|
onCompleted: (data) => {
|
||||||
|
setEntityFields(data?.findManyActivities[0] ?? {});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const activity = data?.findManyActivities[0];
|
const activity = data?.findManyActivities[0];
|
||||||
|
|
||||||
if (!activity) {
|
if (!activity) {
|
||||||
|
|||||||
@ -7,10 +7,9 @@ import { useBoardContext } from '@/ui/board/hooks/useBoardContext';
|
|||||||
import { useCurrentCardSelected } from '@/ui/board/hooks/useCurrentCardSelected';
|
import { useCurrentCardSelected } from '@/ui/board/hooks/useCurrentCardSelected';
|
||||||
import { visibleBoardCardFieldsScopedSelector } from '@/ui/board/states/selectors/visibleBoardCardFieldsScopedSelector';
|
import { visibleBoardCardFieldsScopedSelector } from '@/ui/board/states/selectors/visibleBoardCardFieldsScopedSelector';
|
||||||
import { EntityChipVariant } from '@/ui/chip/components/EntityChip';
|
import { EntityChipVariant } from '@/ui/chip/components/EntityChip';
|
||||||
import { GenericEditableField } from '@/ui/editable-field/components/GenericEditableField';
|
import { InlineCell } from '@/ui/editable-field/components/InlineCell';
|
||||||
import { EditableFieldDefinitionContext } from '@/ui/editable-field/contexts/EditableFieldDefinitionContext';
|
import { EditableFieldHotkeyScope } from '@/ui/editable-field/types/EditableFieldHotkeyScope';
|
||||||
import { EditableFieldEntityIdContext } from '@/ui/editable-field/contexts/EditableFieldEntityIdContext';
|
import { FieldContext } from '@/ui/field/contexts/FieldContext';
|
||||||
import { EditableFieldMutationContext } from '@/ui/editable-field/contexts/EditableFieldMutationContext';
|
|
||||||
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
|
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
|
||||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||||
import { useUpdateOnePipelineProgressMutation } from '~/generated/graphql';
|
import { useUpdateOnePipelineProgressMutation } from '~/generated/graphql';
|
||||||
@ -158,27 +157,28 @@ export const CompanyBoardCard = () => {
|
|||||||
</StyledCheckboxContainer>
|
</StyledCheckboxContainer>
|
||||||
</StyledBoardCardHeader>
|
</StyledBoardCardHeader>
|
||||||
<StyledBoardCardBody>
|
<StyledBoardCardBody>
|
||||||
<EditableFieldMutationContext.Provider
|
{visibleBoardCardFields.map((viewField) => (
|
||||||
value={useUpdateOnePipelineProgressMutation}
|
<PreventSelectOnClickContainer key={viewField.key}>
|
||||||
>
|
<FieldContext.Provider
|
||||||
<EditableFieldEntityIdContext.Provider value={boardCardId}>
|
value={{
|
||||||
{visibleBoardCardFields.map((viewField) => (
|
entityId: boardCardId,
|
||||||
<PreventSelectOnClickContainer key={viewField.key}>
|
recoilScopeId: boardCardId + viewField.key,
|
||||||
<EditableFieldDefinitionContext.Provider
|
fieldDefinition: {
|
||||||
value={{
|
key: viewField.key,
|
||||||
key: viewField.key,
|
name: viewField.name,
|
||||||
name: viewField.name,
|
Icon: viewField.Icon,
|
||||||
Icon: viewField.Icon,
|
type: viewField.type,
|
||||||
type: viewField.metadata.type,
|
metadata: viewField.metadata,
|
||||||
metadata: viewField.metadata,
|
useEditButton: viewField.useEditButton,
|
||||||
}}
|
},
|
||||||
>
|
useUpdateEntityMutation: useUpdateOnePipelineProgressMutation,
|
||||||
<GenericEditableField />
|
hotkeyScope: EditableFieldHotkeyScope.EditableField,
|
||||||
</EditableFieldDefinitionContext.Provider>
|
}}
|
||||||
</PreventSelectOnClickContainer>
|
>
|
||||||
))}
|
<InlineCell />
|
||||||
</EditableFieldEntityIdContext.Provider>
|
</FieldContext.Provider>
|
||||||
</EditableFieldMutationContext.Provider>
|
</PreventSelectOnClickContainer>
|
||||||
|
))}
|
||||||
</StyledBoardCardBody>
|
</StyledBoardCardBody>
|
||||||
</StyledBoardCard>
|
</StyledBoardCard>
|
||||||
</StyledBoardCardWrapper>
|
</StyledBoardCardWrapper>
|
||||||
|
|||||||
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
ViewFieldBooleanMetadata,
|
FieldBooleanMetadata,
|
||||||
ViewFieldChipMetadata,
|
FieldChipMetadata,
|
||||||
ViewFieldDateMetadata,
|
FieldDateMetadata,
|
||||||
ViewFieldMetadata,
|
FieldMetadata,
|
||||||
ViewFieldMoneyMetadata,
|
FieldMoneyMetadata,
|
||||||
ViewFieldNumberMetadata,
|
FieldNumberMetadata,
|
||||||
ViewFieldRelationMetadata,
|
FieldRelationMetadata,
|
||||||
ViewFieldTextMetadata,
|
FieldTextMetadata,
|
||||||
ViewFieldURLMetadata,
|
FieldURLMetadata,
|
||||||
} from '@/ui/editable-field/types/ViewField';
|
} from '@/ui/field/types/FieldMetadata';
|
||||||
import {
|
import {
|
||||||
IconBrandLinkedin,
|
IconBrandLinkedin,
|
||||||
IconBrandX,
|
IconBrandX,
|
||||||
@ -24,7 +24,7 @@ import {
|
|||||||
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
||||||
import { ColumnDefinition } from '@/ui/table/types/ColumnDefinition';
|
import { ColumnDefinition } from '@/ui/table/types/ColumnDefinition';
|
||||||
|
|
||||||
export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadata>[] =
|
export const companiesAvailableColumnDefinitions: ColumnDefinition<FieldMetadata>[] =
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: 'name',
|
key: 'name',
|
||||||
@ -32,125 +32,131 @@ export const companiesAvailableColumnDefinitions: ColumnDefinition<ViewFieldMeta
|
|||||||
Icon: IconBuildingSkyscraper,
|
Icon: IconBuildingSkyscraper,
|
||||||
size: 180,
|
size: 180,
|
||||||
index: 0,
|
index: 0,
|
||||||
|
type: 'chip',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'chip',
|
|
||||||
urlFieldName: 'domainName',
|
urlFieldName: 'domainName',
|
||||||
contentFieldName: 'name',
|
contentFieldName: 'name',
|
||||||
relationType: Entity.Company,
|
relationType: Entity.Company,
|
||||||
|
placeHolder: 'Company Name',
|
||||||
},
|
},
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
} as ColumnDefinition<ViewFieldChipMetadata>,
|
} satisfies ColumnDefinition<FieldChipMetadata>,
|
||||||
{
|
{
|
||||||
key: 'domainName',
|
key: 'domainName',
|
||||||
name: 'URL',
|
name: 'URL',
|
||||||
Icon: IconLink,
|
Icon: IconLink,
|
||||||
size: 100,
|
size: 100,
|
||||||
index: 1,
|
index: 1,
|
||||||
|
type: 'url',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'url',
|
|
||||||
fieldName: 'domainName',
|
fieldName: 'domainName',
|
||||||
placeHolder: 'example.com',
|
placeHolder: 'example.com',
|
||||||
},
|
},
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
} as ColumnDefinition<ViewFieldURLMetadata>,
|
useEditButton: true,
|
||||||
|
} satisfies ColumnDefinition<FieldURLMetadata>,
|
||||||
{
|
{
|
||||||
key: 'accountOwner',
|
key: 'accountOwner',
|
||||||
name: 'Account Owner',
|
name: 'Account Owner',
|
||||||
Icon: IconUserCircle,
|
Icon: IconUserCircle,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 2,
|
index: 2,
|
||||||
|
type: 'relation',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'relation',
|
|
||||||
fieldName: 'accountOwner',
|
fieldName: 'accountOwner',
|
||||||
relationType: Entity.User,
|
relationType: Entity.User,
|
||||||
},
|
},
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
} satisfies ColumnDefinition<ViewFieldRelationMetadata>,
|
} satisfies ColumnDefinition<FieldRelationMetadata>,
|
||||||
{
|
{
|
||||||
key: 'createdAt',
|
key: 'createdAt',
|
||||||
name: 'Creation',
|
name: 'Creation',
|
||||||
Icon: IconCalendarEvent,
|
Icon: IconCalendarEvent,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 3,
|
index: 3,
|
||||||
|
type: 'date',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'date',
|
|
||||||
fieldName: 'createdAt',
|
fieldName: 'createdAt',
|
||||||
},
|
},
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
} satisfies ColumnDefinition<ViewFieldDateMetadata>,
|
} satisfies ColumnDefinition<FieldDateMetadata>,
|
||||||
{
|
{
|
||||||
key: 'employees',
|
key: 'employees',
|
||||||
name: 'Employees',
|
name: 'Employees',
|
||||||
Icon: IconUsers,
|
Icon: IconUsers,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 4,
|
index: 4,
|
||||||
|
type: 'number',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'number',
|
|
||||||
fieldName: 'employees',
|
fieldName: 'employees',
|
||||||
isPositive: true,
|
isPositive: true,
|
||||||
|
placeHolder: 'Employees',
|
||||||
},
|
},
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
} satisfies ColumnDefinition<ViewFieldNumberMetadata>,
|
} satisfies ColumnDefinition<FieldNumberMetadata>,
|
||||||
{
|
{
|
||||||
key: 'linkedin',
|
key: 'linkedin',
|
||||||
name: 'LinkedIn',
|
name: 'LinkedIn',
|
||||||
Icon: IconBrandLinkedin,
|
Icon: IconBrandLinkedin,
|
||||||
size: 170,
|
size: 170,
|
||||||
index: 5,
|
index: 5,
|
||||||
|
type: 'url',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'url',
|
|
||||||
fieldName: 'linkedinUrl',
|
fieldName: 'linkedinUrl',
|
||||||
placeHolder: 'LinkedIn URL',
|
placeHolder: 'LinkedIn URL',
|
||||||
},
|
},
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
} satisfies ColumnDefinition<ViewFieldURLMetadata>,
|
useEditButton: true,
|
||||||
|
} satisfies ColumnDefinition<FieldURLMetadata>,
|
||||||
{
|
{
|
||||||
key: 'address',
|
key: 'address',
|
||||||
name: 'Address',
|
name: 'Address',
|
||||||
Icon: IconMap,
|
Icon: IconMap,
|
||||||
size: 170,
|
size: 170,
|
||||||
index: 6,
|
index: 6,
|
||||||
|
type: 'text',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'text',
|
|
||||||
fieldName: 'address',
|
fieldName: 'address',
|
||||||
placeHolder: 'Address', // Hack: Fake character to prevent password-manager from filling the field
|
placeHolder: 'Address', // Hack: Fake character to prevent password-manager from filling the field
|
||||||
},
|
},
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
} satisfies ColumnDefinition<ViewFieldTextMetadata>,
|
} satisfies ColumnDefinition<FieldTextMetadata>,
|
||||||
{
|
{
|
||||||
key: 'idealCustomerProfile',
|
key: 'idealCustomerProfile',
|
||||||
name: 'ICP',
|
name: 'ICP',
|
||||||
Icon: IconTarget,
|
Icon: IconTarget,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 7,
|
index: 7,
|
||||||
|
type: 'boolean',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'boolean',
|
|
||||||
fieldName: 'idealCustomerProfile',
|
fieldName: 'idealCustomerProfile',
|
||||||
},
|
},
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
} satisfies ColumnDefinition<ViewFieldBooleanMetadata>,
|
} satisfies ColumnDefinition<FieldBooleanMetadata>,
|
||||||
{
|
{
|
||||||
key: 'annualRecurringRevenue',
|
key: 'annualRecurringRevenue',
|
||||||
name: 'ARR',
|
name: 'ARR',
|
||||||
Icon: IconMoneybag,
|
Icon: IconMoneybag,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 8,
|
index: 8,
|
||||||
|
type: 'moneyAmount',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'moneyAmount',
|
|
||||||
fieldName: 'annualRecurringRevenue',
|
fieldName: 'annualRecurringRevenue',
|
||||||
|
placeHolder: 'ARR',
|
||||||
},
|
},
|
||||||
} satisfies ColumnDefinition<ViewFieldMoneyMetadata>,
|
} satisfies ColumnDefinition<FieldMoneyMetadata>,
|
||||||
{
|
{
|
||||||
key: 'xUrl',
|
key: 'xUrl',
|
||||||
name: 'Twitter',
|
name: 'Twitter',
|
||||||
Icon: IconBrandX,
|
Icon: IconBrandX,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 9,
|
index: 9,
|
||||||
|
type: 'url',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'url',
|
|
||||||
fieldName: 'xUrl',
|
fieldName: 'xUrl',
|
||||||
placeHolder: 'X',
|
placeHolder: 'X',
|
||||||
},
|
},
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
} satisfies ColumnDefinition<ViewFieldURLMetadata>,
|
useEditButton: true,
|
||||||
|
} satisfies ColumnDefinition<FieldURLMetadata>,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { genericEntitiesFamilyState } from '@/ui/editable-field/states/genericEntitiesFamilyState';
|
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
|
||||||
import { useGetCompanyQuery } from '~/generated/graphql';
|
import { useGetCompanyQuery } from '~/generated/graphql';
|
||||||
|
|
||||||
export const useCompanyQuery = (id: string) => {
|
export const useCompanyQuery = (id: string) => {
|
||||||
const updateCompanyShowPage = useSetRecoilState(
|
const updateCompanyShowPage = useSetRecoilState(entityFieldsFamilyState(id));
|
||||||
genericEntitiesFamilyState(id),
|
|
||||||
);
|
|
||||||
return useGetCompanyQuery({
|
return useGetCompanyQuery({
|
||||||
variables: { where: { id } },
|
variables: { where: { id } },
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export const useFilteredSearchCompanyQuery = ({
|
|||||||
avatarUrl: getLogoUrlFromDomainName(company.domainName),
|
avatarUrl: getLogoUrlFromDomainName(company.domainName),
|
||||||
domainName: company.domainName,
|
domainName: company.domainName,
|
||||||
avatarType: 'squared',
|
avatarType: 'squared',
|
||||||
|
originalEntity: company,
|
||||||
}),
|
}),
|
||||||
selectedIds: selectedIds,
|
selectedIds: selectedIds,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardId
|
|||||||
import { boardColumnsState } from '@/ui/board/states/boardColumnsState';
|
import { boardColumnsState } from '@/ui/board/states/boardColumnsState';
|
||||||
import { savedBoardColumnsState } from '@/ui/board/states/savedBoardColumnsState';
|
import { savedBoardColumnsState } from '@/ui/board/states/savedBoardColumnsState';
|
||||||
import { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition';
|
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 { isThemeColor } from '@/ui/theme/utils/castStringAsThemeColor';
|
||||||
import { Pipeline } from '~/generated/graphql';
|
import { Pipeline } from '~/generated/graphql';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
@ -72,10 +72,7 @@ export const useUpdateCompanyBoard = () =>
|
|||||||
|
|
||||||
if (!isDeeplyEqual(currentCompanyProgress, companyProgress)) {
|
if (!isDeeplyEqual(currentCompanyProgress, companyProgress)) {
|
||||||
set(companyProgressesFamilyState(id), companyProgress);
|
set(companyProgressesFamilyState(id), companyProgress);
|
||||||
set(
|
set(entityFieldsFamilyState(id), companyProgress.pipelineProgress);
|
||||||
genericEntitiesFamilyState(id),
|
|
||||||
companyProgress.pipelineProgress,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export const CompanyTableMockMode = () => {
|
|||||||
ViewBarRecoilScopeContext: TableRecoilScopeContext,
|
ViewBarRecoilScopeContext: TableRecoilScopeContext,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EntityTable updateEntityMutation={[useUpdateOneCompanyMutation()]} />
|
<EntityTable updateEntityMutation={useUpdateOneCompanyMutation} />
|
||||||
</ViewBarContext.Provider>
|
</ViewBarContext.Provider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -55,6 +55,7 @@ export const PeoplePicker = ({
|
|||||||
name: `${person.firstName} ${person.lastName}`,
|
name: `${person.firstName} ${person.lastName}`,
|
||||||
avatarType: 'rounded',
|
avatarType: 'rounded',
|
||||||
avatarUrl: person.avatarUrl ?? '',
|
avatarUrl: person.avatarUrl ?? '',
|
||||||
|
originalEntity: person,
|
||||||
}),
|
}),
|
||||||
orderByField: 'firstName',
|
orderByField: 'firstName',
|
||||||
excludeEntityIds: excludePersonIds,
|
excludeEntityIds: excludePersonIds,
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
ViewFieldDateMetadata,
|
FieldDateMetadata,
|
||||||
ViewFieldDoubleTextChipMetadata,
|
FieldDoubleTextChipMetadata,
|
||||||
ViewFieldEmailMetadata,
|
FieldEmailMetadata,
|
||||||
ViewFieldMetadata,
|
FieldMetadata,
|
||||||
ViewFieldPhoneMetadata,
|
FieldPhoneMetadata,
|
||||||
ViewFieldRelationMetadata,
|
FieldRelationMetadata,
|
||||||
ViewFieldTextMetadata,
|
FieldTextMetadata,
|
||||||
ViewFieldURLMetadata,
|
FieldURLMetadata,
|
||||||
} from '@/ui/editable-field/types/ViewField';
|
} from '@/ui/field/types/FieldMetadata';
|
||||||
import {
|
import {
|
||||||
IconBrandLinkedin,
|
IconBrandLinkedin,
|
||||||
IconBrandX,
|
IconBrandX,
|
||||||
@ -22,7 +22,7 @@ import {
|
|||||||
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
||||||
import { ColumnDefinition } from '@/ui/table/types/ColumnDefinition';
|
import { ColumnDefinition } from '@/ui/table/types/ColumnDefinition';
|
||||||
|
|
||||||
export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadata>[] =
|
export const peopleAvailableColumnDefinitions: ColumnDefinition<FieldMetadata>[] =
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: 'displayName',
|
key: 'displayName',
|
||||||
@ -30,8 +30,8 @@ export const peopleAvailableColumnDefinitions: ColumnDefinition<ViewFieldMetadat
|
|||||||
Icon: IconUser,
|
Icon: IconUser,
|
||||||
size: 210,
|
size: 210,
|
||||||
index: 0,
|
index: 0,
|
||||||
|
type: 'double-text-chip',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'double-text-chip',
|
|
||||||
firstValueFieldName: 'firstName',
|
firstValueFieldName: 'firstName',
|
||||||
secondValueFieldName: 'lastName',
|
secondValueFieldName: 'lastName',
|
||||||
firstValuePlaceholder: 'First name', // Hack: Fake character to prevent password-manager from filling the field
|
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',
|
avatarUrlFieldName: 'avatarUrl',
|
||||||
entityType: Entity.Person,
|
entityType: Entity.Person,
|
||||||
},
|
},
|
||||||
} satisfies ColumnDefinition<ViewFieldDoubleTextChipMetadata>,
|
} satisfies ColumnDefinition<FieldDoubleTextChipMetadata>,
|
||||||
{
|
{
|
||||||
key: 'email',
|
key: 'email',
|
||||||
name: 'Email',
|
name: 'Email',
|
||||||
Icon: IconMail,
|
Icon: IconMail,
|
||||||
size: 150,
|
size: 150,
|
||||||
|
type: 'email',
|
||||||
index: 1,
|
index: 1,
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'email',
|
|
||||||
fieldName: 'email',
|
fieldName: 'email',
|
||||||
placeHolder: 'Email', // Hack: Fake character to prevent password-manager from filling the field
|
placeHolder: 'Email', // Hack: Fake character to prevent password-manager from filling the field
|
||||||
},
|
},
|
||||||
} satisfies ColumnDefinition<ViewFieldEmailMetadata>,
|
useEditButton: true,
|
||||||
|
} satisfies ColumnDefinition<FieldEmailMetadata>,
|
||||||
{
|
{
|
||||||
key: 'company',
|
key: 'company',
|
||||||
name: 'Company',
|
name: 'Company',
|
||||||
Icon: IconBuildingSkyscraper,
|
Icon: IconBuildingSkyscraper,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 2,
|
index: 2,
|
||||||
|
type: 'relation',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'relation',
|
|
||||||
fieldName: 'company',
|
fieldName: 'company',
|
||||||
relationType: Entity.Company,
|
relationType: Entity.Company,
|
||||||
},
|
},
|
||||||
} satisfies ColumnDefinition<ViewFieldRelationMetadata>,
|
} satisfies ColumnDefinition<FieldRelationMetadata>,
|
||||||
{
|
{
|
||||||
key: 'phone',
|
key: 'phone',
|
||||||
name: 'Phone',
|
name: 'Phone',
|
||||||
Icon: IconPhone,
|
Icon: IconPhone,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 3,
|
index: 3,
|
||||||
|
type: 'phone',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'phone',
|
|
||||||
fieldName: 'phone',
|
fieldName: 'phone',
|
||||||
placeHolder: 'Phone', // Hack: Fake character to prevent password-manager from filling the field
|
placeHolder: 'Phone', // Hack: Fake character to prevent password-manager from filling the field
|
||||||
},
|
},
|
||||||
} satisfies ColumnDefinition<ViewFieldPhoneMetadata>,
|
useEditButton: true,
|
||||||
|
} satisfies ColumnDefinition<FieldPhoneMetadata>,
|
||||||
{
|
{
|
||||||
key: 'createdAt',
|
key: 'createdAt',
|
||||||
name: 'Creation',
|
name: 'Creation',
|
||||||
Icon: IconCalendarEvent,
|
Icon: IconCalendarEvent,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 4,
|
index: 4,
|
||||||
|
type: 'date',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'date',
|
|
||||||
fieldName: 'createdAt',
|
fieldName: 'createdAt',
|
||||||
},
|
},
|
||||||
} satisfies ColumnDefinition<ViewFieldDateMetadata>,
|
} satisfies ColumnDefinition<FieldDateMetadata>,
|
||||||
{
|
{
|
||||||
key: 'city',
|
key: 'city',
|
||||||
name: 'City',
|
name: 'City',
|
||||||
Icon: IconMap,
|
Icon: IconMap,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 5,
|
index: 5,
|
||||||
|
type: 'text',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'text',
|
|
||||||
fieldName: 'city',
|
fieldName: 'city',
|
||||||
placeHolder: 'City', // Hack: Fake character to prevent password-manager from filling the field
|
placeHolder: 'City', // Hack: Fake character to prevent password-manager from filling the field
|
||||||
},
|
},
|
||||||
} satisfies ColumnDefinition<ViewFieldTextMetadata>,
|
} satisfies ColumnDefinition<FieldTextMetadata>,
|
||||||
{
|
{
|
||||||
key: 'jobTitle',
|
key: 'jobTitle',
|
||||||
name: 'Job title',
|
name: 'Job title',
|
||||||
Icon: IconBriefcase,
|
Icon: IconBriefcase,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 6,
|
index: 6,
|
||||||
|
type: 'text',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'text',
|
|
||||||
fieldName: 'jobTitle',
|
fieldName: 'jobTitle',
|
||||||
placeHolder: 'Job title',
|
placeHolder: 'Job title',
|
||||||
},
|
},
|
||||||
} satisfies ColumnDefinition<ViewFieldTextMetadata>,
|
} satisfies ColumnDefinition<FieldTextMetadata>,
|
||||||
{
|
{
|
||||||
key: 'linkedin',
|
key: 'linkedin',
|
||||||
name: 'LinkedIn',
|
name: 'LinkedIn',
|
||||||
Icon: IconBrandLinkedin,
|
Icon: IconBrandLinkedin,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 7,
|
index: 7,
|
||||||
|
type: 'url',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'url',
|
|
||||||
fieldName: 'linkedinUrl',
|
fieldName: 'linkedinUrl',
|
||||||
placeHolder: 'LinkedIn',
|
placeHolder: 'LinkedIn',
|
||||||
},
|
},
|
||||||
} satisfies ColumnDefinition<ViewFieldURLMetadata>,
|
useEditButton: true,
|
||||||
|
} satisfies ColumnDefinition<FieldURLMetadata>,
|
||||||
{
|
{
|
||||||
key: 'x',
|
key: 'x',
|
||||||
name: 'Twitter',
|
name: 'Twitter',
|
||||||
Icon: IconBrandX,
|
Icon: IconBrandX,
|
||||||
size: 150,
|
size: 150,
|
||||||
index: 8,
|
index: 8,
|
||||||
|
type: 'url',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'url',
|
|
||||||
fieldName: 'xUrl',
|
fieldName: 'xUrl',
|
||||||
placeHolder: 'X',
|
placeHolder: 'X',
|
||||||
},
|
},
|
||||||
} satisfies ColumnDefinition<ViewFieldURLMetadata>,
|
useEditButton: true,
|
||||||
|
} satisfies ColumnDefinition<FieldURLMetadata>,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
|
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 { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||||
import { Person, useUpdateOnePersonMutation } from '~/generated/graphql';
|
import { Person, useUpdateOnePersonMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ export const PeopleFullNameEditableField = ({ people }: OwnProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
|
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
|
||||||
<DoubleTextInputEdit
|
<EntityTitleDoubleTextInput
|
||||||
firstValuePlaceholder="Empty"
|
firstValuePlaceholder="Empty"
|
||||||
secondValuePlaceholder="Empty"
|
secondValuePlaceholder="Empty"
|
||||||
firstValue={internalValueFirstName ?? ''}
|
firstValue={internalValueFirstName ?? ''}
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import {
|
|||||||
ActivityTargetableEntity,
|
ActivityTargetableEntity,
|
||||||
ActivityTargetableEntityType,
|
ActivityTargetableEntityType,
|
||||||
} from '@/activities/types/ActivityTargetableEntity';
|
} from '@/activities/types/ActivityTargetableEntity';
|
||||||
|
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
|
||||||
import { selectedRowIdsSelector } from '@/ui/table/states/selectors/selectedRowIdsSelector';
|
import { selectedRowIdsSelector } from '@/ui/table/states/selectors/selectedRowIdsSelector';
|
||||||
import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState';
|
|
||||||
import { ActivityType, Person } from '~/generated/graphql';
|
import { ActivityType, Person } from '~/generated/graphql';
|
||||||
|
|
||||||
export const useCreateActivityForPeople = () => {
|
export const useCreateActivityForPeople = () => {
|
||||||
@ -20,7 +20,7 @@ export const useCreateActivityForPeople = () => {
|
|||||||
const relatedEntites: ActivityTargetableEntity[] = [];
|
const relatedEntites: ActivityTargetableEntity[] = [];
|
||||||
for (const id of selectedRowIds) {
|
for (const id of selectedRowIds) {
|
||||||
const person = snapshot
|
const person = snapshot
|
||||||
.getLoadable(tableEntitiesFamilyState(id))
|
.getLoadable(entityFieldsFamilyState(id))
|
||||||
.getValue() as Person;
|
.getValue() as Person;
|
||||||
if (
|
if (
|
||||||
person?.company?.id &&
|
person?.company?.id &&
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { genericEntitiesFamilyState } from '@/ui/editable-field/states/genericEntitiesFamilyState';
|
import { entityFieldsFamilyState } from '@/ui/field/states/entityFieldsFamilyState';
|
||||||
import { useGetPersonQuery } from '~/generated/graphql';
|
import { useGetPersonQuery } from '~/generated/graphql';
|
||||||
|
|
||||||
export const usePersonQuery = (id: string) => {
|
export const usePersonQuery = (id: string) => {
|
||||||
const updatePersonShowPage = useSetRecoilState(
|
const updatePersonShowPage = useSetRecoilState(entityFieldsFamilyState(id));
|
||||||
genericEntitiesFamilyState(id),
|
|
||||||
);
|
|
||||||
return useGetPersonQuery({
|
return useGetPersonQuery({
|
||||||
variables: { id },
|
variables: { id },
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
|
import { BoardFieldDefinition } from '@/ui/board/types/BoardFieldDefinition';
|
||||||
import {
|
import {
|
||||||
ViewFieldDateMetadata,
|
FieldDateMetadata,
|
||||||
ViewFieldDefinition,
|
FieldMetadata,
|
||||||
ViewFieldMetadata,
|
FieldNumberMetadata,
|
||||||
ViewFieldNumberMetadata,
|
FieldProbabilityMetadata,
|
||||||
ViewFieldProbabilityMetadata,
|
FieldRelationMetadata,
|
||||||
ViewFieldRelationMetadata,
|
} from '@/ui/field/types/FieldMetadata';
|
||||||
} from '@/ui/editable-field/types/ViewField';
|
|
||||||
import {
|
import {
|
||||||
IconCalendarEvent,
|
IconCalendarEvent,
|
||||||
IconCurrencyDollar,
|
IconCurrencyDollar,
|
||||||
@ -14,52 +14,54 @@ import {
|
|||||||
} from '@/ui/icon';
|
} from '@/ui/icon';
|
||||||
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
||||||
|
|
||||||
export const pipelineAvailableFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[] =
|
export const pipelineAvailableFieldDefinitions: BoardFieldDefinition<FieldMetadata>[] =
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
key: 'closeDate',
|
key: 'closeDate',
|
||||||
name: 'Close Date',
|
name: 'Close Date',
|
||||||
Icon: IconCalendarEvent,
|
Icon: IconCalendarEvent,
|
||||||
index: 0,
|
index: 0,
|
||||||
|
type: 'date',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'date',
|
|
||||||
fieldName: 'closeDate',
|
fieldName: 'closeDate',
|
||||||
},
|
},
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
} satisfies ViewFieldDefinition<ViewFieldDateMetadata>,
|
} satisfies BoardFieldDefinition<FieldDateMetadata>,
|
||||||
{
|
{
|
||||||
key: 'amount',
|
key: 'amount',
|
||||||
name: 'Amount',
|
name: 'Amount',
|
||||||
Icon: IconCurrencyDollar,
|
Icon: IconCurrencyDollar,
|
||||||
index: 1,
|
index: 1,
|
||||||
|
type: 'number',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'number',
|
|
||||||
fieldName: 'amount',
|
fieldName: 'amount',
|
||||||
|
placeHolder: '0',
|
||||||
},
|
},
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
} satisfies ViewFieldDefinition<ViewFieldNumberMetadata>,
|
} satisfies BoardFieldDefinition<FieldNumberMetadata>,
|
||||||
{
|
{
|
||||||
key: 'probability',
|
key: 'probability',
|
||||||
name: 'Probability',
|
name: 'Probability',
|
||||||
Icon: IconProgressCheck,
|
Icon: IconProgressCheck,
|
||||||
index: 2,
|
index: 2,
|
||||||
|
type: 'probability',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'probability',
|
|
||||||
fieldName: 'probability',
|
fieldName: 'probability',
|
||||||
},
|
},
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
} satisfies ViewFieldDefinition<ViewFieldProbabilityMetadata>,
|
} satisfies BoardFieldDefinition<FieldProbabilityMetadata>,
|
||||||
{
|
{
|
||||||
key: 'pointOfContact',
|
key: 'pointOfContact',
|
||||||
name: 'Point of Contact',
|
name: 'Point of Contact',
|
||||||
Icon: IconUser,
|
Icon: IconUser,
|
||||||
index: 3,
|
index: 3,
|
||||||
|
type: 'relation',
|
||||||
metadata: {
|
metadata: {
|
||||||
type: 'relation',
|
|
||||||
fieldName: 'pointOfContact',
|
fieldName: 'pointOfContact',
|
||||||
relationType: Entity.Person,
|
relationType: Entity.Person,
|
||||||
useEditButton: true,
|
useEditButton: true,
|
||||||
},
|
},
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
} satisfies ViewFieldDefinition<ViewFieldRelationMetadata>,
|
useEditButton: true,
|
||||||
|
} satisfies BoardFieldDefinition<FieldRelationMetadata>,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,39 +1,26 @@
|
|||||||
import {
|
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
} from '@/ui/editable-field/types/ViewField';
|
|
||||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
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 { boardCardFieldsScopedState } from '../states/boardCardFieldsScopedState';
|
||||||
import { boardCardFieldsByKeyScopedSelector } from '../states/selectors/boardCardFieldsByKeyScopedSelector';
|
|
||||||
|
|
||||||
import { useBoardContext } from './useBoardContext';
|
import { useBoardContext } from './useBoardContext';
|
||||||
|
|
||||||
export const useBoardCardFields = () => {
|
export const useBoardCardFields = () => {
|
||||||
const { BoardRecoilScopeContext } = useBoardContext();
|
const { BoardRecoilScopeContext } = useBoardContext();
|
||||||
|
|
||||||
const [boardCardFields, setBoardCardFields] = useRecoilScopedState(
|
const [, setBoardCardFields] = useRecoilScopedState(
|
||||||
boardCardFieldsScopedState,
|
boardCardFieldsScopedState,
|
||||||
BoardRecoilScopeContext,
|
BoardRecoilScopeContext,
|
||||||
);
|
);
|
||||||
const boardCardFieldsByKey = useRecoilScopedValue(
|
|
||||||
boardCardFieldsByKeyScopedSelector,
|
|
||||||
BoardRecoilScopeContext,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFieldVisibilityChange = (
|
const handleFieldVisibilityChange = (field: ViewFieldForVisibility) => {
|
||||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
setBoardCardFields((previousFields) =>
|
||||||
) => {
|
previousFields.map((previousField) =>
|
||||||
const nextFields = boardCardFieldsByKey[field.key]
|
previousField.key === field.key
|
||||||
? boardCardFields.map((previousField) =>
|
? { ...previousField, isVisible: !field.isVisible }
|
||||||
previousField.key === field.key
|
: previousField,
|
||||||
? { ...previousField, isVisible: !field.isVisible }
|
),
|
||||||
: previousField,
|
);
|
||||||
)
|
|
||||||
: [...boardCardFields, { ...field, isVisible: true }];
|
|
||||||
|
|
||||||
setBoardCardFields(nextFields);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { handleFieldVisibilityChange };
|
return { handleFieldVisibilityChange };
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { atomFamily } from 'recoil';
|
import { atomFamily } from 'recoil';
|
||||||
|
|
||||||
import {
|
import { FieldMetadata } from '@/ui/field/types/FieldMetadata';
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
|
||||||
} from '@/ui/editable-field/types/ViewField';
|
|
||||||
|
|
||||||
export const availableBoardCardFieldsScopedState = atomFamily<
|
export const availableBoardCardFieldsScopedState = atomFamily<
|
||||||
ViewFieldDefinition<ViewFieldMetadata>[],
|
BoardFieldDefinition<FieldMetadata>[],
|
||||||
string
|
string
|
||||||
>({
|
>({
|
||||||
key: 'availableBoardCardFieldsScopedState',
|
key: 'availableBoardCardFieldsScopedState',
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { atomFamily } from 'recoil';
|
import { atomFamily } from 'recoil';
|
||||||
|
|
||||||
import {
|
import { FieldMetadata } from '@/ui/field/types/FieldMetadata';
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
|
||||||
} from '@/ui/editable-field/types/ViewField';
|
|
||||||
|
|
||||||
export const boardCardFieldsScopedState = atomFamily<
|
export const boardCardFieldsScopedState = atomFamily<
|
||||||
ViewFieldDefinition<ViewFieldMetadata>[],
|
BoardFieldDefinition<FieldMetadata>[],
|
||||||
string
|
string
|
||||||
>({
|
>({
|
||||||
key: 'boardCardFieldsScopedState',
|
key: 'boardCardFieldsScopedState',
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { atomFamily } from 'recoil';
|
import { atomFamily } from 'recoil';
|
||||||
|
|
||||||
import {
|
import { FieldMetadata } from '@/ui/field/types/FieldMetadata';
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
import { BoardFieldDefinition } from '../types/BoardFieldDefinition';
|
||||||
} from '@/ui/editable-field/types/ViewField';
|
|
||||||
|
|
||||||
export const savedBoardCardFieldsFamilyState = atomFamily<
|
export const savedBoardCardFieldsFamilyState = atomFamily<
|
||||||
ViewFieldDefinition<ViewFieldMetadata>[],
|
BoardFieldDefinition<FieldMetadata>[],
|
||||||
string | undefined
|
string | undefined
|
||||||
>({
|
>({
|
||||||
key: 'savedBoardCardFieldsFamilyState',
|
key: 'savedBoardCardFieldsFamilyState',
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { selectorFamily } from 'recoil';
|
import { selectorFamily } from 'recoil';
|
||||||
|
|
||||||
import {
|
import { FieldMetadata } from '@/ui/field/types/FieldMetadata';
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
} from '@/ui/editable-field/types/ViewField';
|
|
||||||
|
|
||||||
|
import { BoardFieldDefinition } from '../../types/BoardFieldDefinition';
|
||||||
import { boardCardFieldsScopedState } from '../boardCardFieldsScopedState';
|
import { boardCardFieldsScopedState } from '../boardCardFieldsScopedState';
|
||||||
|
|
||||||
export const boardCardFieldsByKeyScopedSelector = selectorFamily({
|
export const boardCardFieldsByKeyScopedSelector = selectorFamily({
|
||||||
@ -13,6 +11,6 @@ export const boardCardFieldsByKeyScopedSelector = selectorFamily({
|
|||||||
(scopeId: string) =>
|
(scopeId: string) =>
|
||||||
({ get }) =>
|
({ get }) =>
|
||||||
get(boardCardFieldsScopedState(scopeId)).reduce<
|
get(boardCardFieldsScopedState(scopeId)).reduce<
|
||||||
Record<string, ViewFieldDefinition<ViewFieldMetadata>>
|
Record<string, BoardFieldDefinition<FieldMetadata>>
|
||||||
>((result, field) => ({ ...result, [field.key]: field }), {}),
|
>((result, field) => ({ ...result, [field.key]: field }), {}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { selectorFamily } from 'recoil';
|
import { selectorFamily } from 'recoil';
|
||||||
|
|
||||||
import {
|
import { FieldMetadata } from '@/ui/field/types/FieldMetadata';
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
} from '@/ui/editable-field/types/ViewField';
|
|
||||||
|
|
||||||
|
import { BoardFieldDefinition } from '../../types/BoardFieldDefinition';
|
||||||
import { savedBoardCardFieldsFamilyState } from '../savedBoardCardFieldsFamilyState';
|
import { savedBoardCardFieldsFamilyState } from '../savedBoardCardFieldsFamilyState';
|
||||||
|
|
||||||
export const savedBoardCardFieldsByKeyFamilySelector = selectorFamily({
|
export const savedBoardCardFieldsByKeyFamilySelector = selectorFamily({
|
||||||
@ -13,6 +11,6 @@ export const savedBoardCardFieldsByKeyFamilySelector = selectorFamily({
|
|||||||
(viewId: string | undefined) =>
|
(viewId: string | undefined) =>
|
||||||
({ get }) =>
|
({ get }) =>
|
||||||
get(savedBoardCardFieldsFamilyState(viewId)).reduce<
|
get(savedBoardCardFieldsFamilyState(viewId)).reduce<
|
||||||
Record<string, ViewFieldDefinition<ViewFieldMetadata>>
|
Record<string, BoardFieldDefinition<FieldMetadata>>
|
||||||
>((result, field) => ({ ...result, [field.key]: field }), {}),
|
>((result, field) => ({ ...result, [field.key]: field }), {}),
|
||||||
});
|
});
|
||||||
|
|||||||
8
front/src/modules/ui/board/types/BoardFieldDefinition.ts
Normal file
8
front/src/modules/ui/board/types/BoardFieldDefinition.ts
Normal 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;
|
||||||
|
};
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Keys } from 'react-hotkeys-hook';
|
import { Keys } from 'react-hotkeys-hook';
|
||||||
import { flip, offset, Placement, useFloating } from '@floating-ui/react';
|
import { flip, offset, Placement, useFloating } from '@floating-ui/react';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
|
import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
@ -71,7 +72,7 @@ export const DropdownButton = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
'esc',
|
Key.Escape,
|
||||||
() => {
|
() => {
|
||||||
closeDropdownButton();
|
closeDropdownButton();
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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} />;
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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 <></>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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 <> </>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,15 +1,16 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
|
|
||||||
import { useEditableField } from '../hooks/useEditableField';
|
import { useInlineCell } from '../hooks/useInlineCell';
|
||||||
|
|
||||||
import { EditableFieldDisplayMode } from './EditableFieldDisplayMode';
|
import { InlineCellDisplayMode } from './InlineCellDisplayMode';
|
||||||
import { EditableFieldEditButton } from './EditableFieldEditButton';
|
import { InlineCellEditButton } from './InlineCellEditButton';
|
||||||
import { EditableFieldEditMode } from './EditableFieldEditMode';
|
import { InlineCellEditMode } from './InlineCellEditMode';
|
||||||
|
|
||||||
const StyledIconContainer = styled.div`
|
const StyledIconContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -57,7 +58,7 @@ const StyledClickableContainer = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledEditableFieldBaseContainer = styled.div`
|
const StyledInlineCellBaseContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ type OwnProps = {
|
|||||||
labelFixedWidth?: number;
|
labelFixedWidth?: number;
|
||||||
useEditButton?: boolean;
|
useEditButton?: boolean;
|
||||||
editModeContent?: React.ReactNode;
|
editModeContent?: React.ReactNode;
|
||||||
displayModeContentOnly?: boolean;
|
editModeContentOnly?: boolean;
|
||||||
displayModeContent: React.ReactNode;
|
displayModeContent: React.ReactNode;
|
||||||
customEditHotkeyScope?: HotkeyScope;
|
customEditHotkeyScope?: HotkeyScope;
|
||||||
isDisplayModeContentEmpty?: boolean;
|
isDisplayModeContentEmpty?: boolean;
|
||||||
@ -85,7 +86,7 @@ type OwnProps = {
|
|||||||
disableHoverEffect?: boolean;
|
disableHoverEffect?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditableField = ({
|
export const InlineCellContainer = ({
|
||||||
IconLabel,
|
IconLabel,
|
||||||
label,
|
label,
|
||||||
labelFixedWidth,
|
labelFixedWidth,
|
||||||
@ -94,7 +95,7 @@ export const EditableField = ({
|
|||||||
displayModeContent,
|
displayModeContent,
|
||||||
customEditHotkeyScope,
|
customEditHotkeyScope,
|
||||||
isDisplayModeContentEmpty,
|
isDisplayModeContentEmpty,
|
||||||
displayModeContentOnly,
|
editModeContentOnly,
|
||||||
isDisplayModeFixHeight,
|
isDisplayModeFixHeight,
|
||||||
disableHoverEffect,
|
disableHoverEffect,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
@ -108,46 +109,61 @@ export const EditableField = ({
|
|||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { isFieldInEditMode, openEditableField } = useEditableField();
|
const { isInlineCellInEditMode, openInlineCell } = useInlineCell();
|
||||||
|
|
||||||
const handleDisplayModeClick = () => {
|
const handleDisplayModeClick = () => {
|
||||||
if (!displayModeContentOnly) {
|
if (!editModeContentOnly) {
|
||||||
openEditableField(customEditHotkeyScope);
|
openInlineCell(customEditHotkeyScope);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEditButton =
|
const showEditButton =
|
||||||
!isFieldInEditMode && isHovered && useEditButton && !displayModeContentOnly;
|
!isInlineCellInEditMode &&
|
||||||
|
isHovered &&
|
||||||
|
useEditButton &&
|
||||||
|
!editModeContentOnly;
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledEditableFieldBaseContainer
|
<StyledInlineCellBaseContainer
|
||||||
onMouseEnter={handleContainerMouseEnter}
|
onMouseEnter={handleContainerMouseEnter}
|
||||||
onMouseLeave={handleContainerMouseLeave}
|
onMouseLeave={handleContainerMouseLeave}
|
||||||
>
|
>
|
||||||
<StyledLabelAndIconContainer>
|
<StyledLabelAndIconContainer>
|
||||||
{IconLabel && (
|
{IconLabel && (
|
||||||
<StyledIconContainer>
|
<StyledIconContainer>
|
||||||
<IconLabel />
|
<IconLabel stroke={theme.icon.stroke.sm} />
|
||||||
</StyledIconContainer>
|
</StyledIconContainer>
|
||||||
)}
|
)}
|
||||||
{label && (
|
{label && (
|
||||||
<StyledLabel labelFixedWidth={labelFixedWidth}>{label}</StyledLabel>
|
<StyledLabel labelFixedWidth={labelFixedWidth}>{label}</StyledLabel>
|
||||||
)}
|
)}
|
||||||
</StyledLabelAndIconContainer>
|
</StyledLabelAndIconContainer>
|
||||||
|
|
||||||
<StyledValueContainer>
|
<StyledValueContainer>
|
||||||
{isFieldInEditMode ? (
|
{isInlineCellInEditMode ? (
|
||||||
<EditableFieldEditMode>{editModeContent}</EditableFieldEditMode>
|
<InlineCellEditMode>{editModeContent}</InlineCellEditMode>
|
||||||
|
) : editModeContentOnly ? (
|
||||||
|
<StyledClickableContainer>
|
||||||
|
<InlineCellDisplayMode
|
||||||
|
disableHoverEffect={disableHoverEffect}
|
||||||
|
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
||||||
|
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
||||||
|
isHovered={isHovered}
|
||||||
|
>
|
||||||
|
{editModeContent}
|
||||||
|
</InlineCellDisplayMode>
|
||||||
|
</StyledClickableContainer>
|
||||||
) : (
|
) : (
|
||||||
<StyledClickableContainer onClick={handleDisplayModeClick}>
|
<StyledClickableContainer onClick={handleDisplayModeClick}>
|
||||||
<EditableFieldDisplayMode
|
<InlineCellDisplayMode
|
||||||
disableHoverEffect={disableHoverEffect}
|
disableHoverEffect={disableHoverEffect}
|
||||||
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
||||||
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
>
|
>
|
||||||
{displayModeContent}
|
{displayModeContent}
|
||||||
</EditableFieldDisplayMode>
|
</InlineCellDisplayMode>
|
||||||
{showEditButton && (
|
{showEditButton && (
|
||||||
<StyledEditButtonContainer
|
<StyledEditButtonContainer
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
@ -155,12 +171,12 @@ export const EditableField = ({
|
|||||||
transition={{ duration: 0.1 }}
|
transition={{ duration: 0.1 }}
|
||||||
whileHover={{ scale: 1.04 }}
|
whileHover={{ scale: 1.04 }}
|
||||||
>
|
>
|
||||||
<EditableFieldEditButton />
|
<InlineCellEditButton />
|
||||||
</StyledEditButtonContainer>
|
</StyledEditButtonContainer>
|
||||||
)}
|
)}
|
||||||
</StyledClickableContainer>
|
</StyledClickableContainer>
|
||||||
)}
|
)}
|
||||||
</StyledValueContainer>
|
</StyledValueContainer>
|
||||||
</StyledEditableFieldBaseContainer>
|
</StyledInlineCellBaseContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -57,7 +57,7 @@ type OwnProps = {
|
|||||||
isHovered?: boolean;
|
isHovered?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditableFieldDisplayMode = ({
|
export const InlineCellDisplayMode = ({
|
||||||
children,
|
children,
|
||||||
isDisplayModeContentEmpty,
|
isDisplayModeContentEmpty,
|
||||||
disableHoverEffect,
|
disableHoverEffect,
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
|
import { FloatingIconButton } from '@/ui/button/components/FloatingIconButton';
|
||||||
import { IconPencil } from '@/ui/icon';
|
import { IconPencil } from '@/ui/icon';
|
||||||
|
|
||||||
import { useEditableField } from '../hooks/useEditableField';
|
import { useInlineCell } from '../hooks/useInlineCell';
|
||||||
|
|
||||||
export const EditableFieldEditButton = () => {
|
export const InlineCellEditButton = () => {
|
||||||
const { openEditableField } = useEditableField();
|
const { openInlineCell } = useInlineCell();
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
openEditableField();
|
openInlineCell();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -15,7 +15,7 @@ export const EditableFieldEditButton = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
Icon={IconPencil}
|
Icon={IconPencil}
|
||||||
data-testid="editable-field-edit-mode-container"
|
data-testid="inline-cell-edit-mode-container"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const StyledEditableFieldEditModeContainer = styled.div<OwnProps>`
|
const StyledInlineCellEditModeContainer = styled.div<OwnProps>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -11,7 +11,7 @@ const StyledEditableFieldEditModeContainer = styled.div<OwnProps>`
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledEditableFieldInput = styled.div`
|
const StyledInlineCellInput = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: ${({ theme }) => theme.background.transparent.secondary};
|
background: ${({ theme }) => theme.background.transparent.secondary};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
@ -30,8 +30,8 @@ type OwnProps = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditableFieldEditMode = ({ children }: OwnProps) => (
|
export const InlineCellEditMode = ({ children }: OwnProps) => (
|
||||||
<StyledEditableFieldEditModeContainer data-testid="editable-field-edit-mode-container">
|
<StyledInlineCellEditModeContainer data-testid="inline-cell-edit-mode-container">
|
||||||
<StyledEditableFieldInput>{children}</StyledEditableFieldInput>
|
<StyledInlineCellInput>{children}</StyledInlineCellInput>
|
||||||
</StyledEditableFieldEditModeContainer>
|
</StyledInlineCellEditModeContainer>
|
||||||
);
|
);
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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>);
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import { createContext } from 'react';
|
|
||||||
|
|
||||||
export const EditableFieldEntityIdContext = createContext<string>('');
|
|
||||||
@ -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();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
|
||||||
|
|
||||||
import { isFieldInEditModeScopedState } from '../states/isFieldInEditModeScopedState';
|
import { isInlineCellInEditModeScopedState } from '../states/isInlineCellInEditModeScopedState';
|
||||||
import { FieldRecoilScopeContext } from '../states/recoil-scope-contexts/FieldRecoilScopeContext';
|
|
||||||
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
|
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
|
||||||
|
|
||||||
export const useEditableField = () => {
|
export const useInlineCell = () => {
|
||||||
const [isFieldInEditMode, setIsFieldInEditMode] = useRecoilScopedState(
|
const { recoilScopeId } = useContext(FieldContext);
|
||||||
isFieldInEditModeScopedState,
|
|
||||||
FieldRecoilScopeContext,
|
const [isInlineCellInEditMode, setIsInlineCellInEditMode] = useRecoilState(
|
||||||
|
isInlineCellInEditModeScopedState(recoilScopeId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -17,14 +20,14 @@ export const useEditableField = () => {
|
|||||||
goBackToPreviousHotkeyScope,
|
goBackToPreviousHotkeyScope,
|
||||||
} = usePreviousHotkeyScope();
|
} = usePreviousHotkeyScope();
|
||||||
|
|
||||||
const closeEditableField = () => {
|
const closeInlineCell = () => {
|
||||||
setIsFieldInEditMode(false);
|
setIsInlineCellInEditMode(false);
|
||||||
|
|
||||||
goBackToPreviousHotkeyScope();
|
goBackToPreviousHotkeyScope();
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEditableField = (customEditHotkeyScopeForField?: HotkeyScope) => {
|
const openInlineCell = (customEditHotkeyScopeForField?: HotkeyScope) => {
|
||||||
setIsFieldInEditMode(true);
|
setIsInlineCellInEditMode(true);
|
||||||
|
|
||||||
if (customEditHotkeyScopeForField) {
|
if (customEditHotkeyScopeForField) {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(
|
setHotkeyScopeAndMemorizePreviousScope(
|
||||||
@ -39,8 +42,8 @@ export const useEditableField = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isFieldInEditMode,
|
isInlineCellInEditMode,
|
||||||
closeEditableField,
|
closeInlineCell,
|
||||||
openEditableField,
|
openInlineCell,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -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],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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;
|
|
||||||
};
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { atomFamily } from 'recoil';
|
|
||||||
|
|
||||||
export const genericEntitiesFamilyState = atomFamily<
|
|
||||||
Record<string, unknown> | null,
|
|
||||||
string
|
|
||||||
>({
|
|
||||||
key: 'genericEntitiesFamilyState',
|
|
||||||
default: null,
|
|
||||||
});
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { atomFamily } from 'recoil';
|
|
||||||
|
|
||||||
export const isFieldInEditModeScopedState = atomFamily<boolean, string>({
|
|
||||||
key: 'isFieldInEditModeScopedState',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { atomFamily } from 'recoil';
|
||||||
|
|
||||||
|
export const isInlineCellInEditModeScopedState = atomFamily<boolean, string>({
|
||||||
|
key: 'isInlineCellInEditModeScopedState',
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
@ -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,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
@ -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;
|
|
||||||
};
|
|
||||||
@ -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;
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import {
|
|
||||||
ViewFieldBooleanMetadata,
|
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
} from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldBoolean = (
|
|
||||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
|
||||||
): field is ViewFieldDefinition<ViewFieldBooleanMetadata> =>
|
|
||||||
field.metadata.type === 'boolean';
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import { ViewFieldBooleanValue } from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldBooleanValue = (
|
|
||||||
fieldValue: unknown,
|
|
||||||
): fieldValue is ViewFieldBooleanValue => typeof fieldValue === 'boolean';
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import {
|
|
||||||
ViewFieldChipMetadata,
|
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
} from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldChip = (
|
|
||||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
|
||||||
): field is ViewFieldDefinition<ViewFieldChipMetadata> =>
|
|
||||||
field.metadata.type === 'chip';
|
|
||||||
@ -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';
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import {
|
|
||||||
ViewFieldDateMetadata,
|
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
} from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldDate = (
|
|
||||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
|
||||||
): field is ViewFieldDefinition<ViewFieldDateMetadata> =>
|
|
||||||
field.metadata.type === 'date';
|
|
||||||
@ -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';
|
|
||||||
@ -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';
|
|
||||||
@ -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';
|
|
||||||
@ -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';
|
|
||||||
@ -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';
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import {
|
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldEmailMetadata,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
} from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldEmail = (
|
|
||||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
|
||||||
): field is ViewFieldDefinition<ViewFieldEmailMetadata> =>
|
|
||||||
field.metadata.type === 'email';
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { ViewFieldEmailValue } from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldEmailValue = (
|
|
||||||
fieldValue: unknown,
|
|
||||||
): fieldValue is ViewFieldEmailValue =>
|
|
||||||
fieldValue !== null &&
|
|
||||||
fieldValue !== undefined &&
|
|
||||||
typeof fieldValue === 'string';
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import {
|
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
ViewFieldMoneyMetadata,
|
|
||||||
} from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldMoney = (
|
|
||||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
|
||||||
): field is ViewFieldDefinition<ViewFieldMoneyMetadata> =>
|
|
||||||
field.metadata.type === 'moneyAmount';
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { ViewFieldMoneyValue } from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldMoneyValue = (
|
|
||||||
fieldValue: unknown,
|
|
||||||
): fieldValue is ViewFieldMoneyValue =>
|
|
||||||
fieldValue === null ||
|
|
||||||
(fieldValue !== undefined && typeof fieldValue === 'number');
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import {
|
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
ViewFieldNumberMetadata,
|
|
||||||
} from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldNumber = (
|
|
||||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
|
||||||
): field is ViewFieldDefinition<ViewFieldNumberMetadata> =>
|
|
||||||
field.metadata.type === 'number';
|
|
||||||
@ -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';
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import {
|
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
ViewFieldPhoneMetadata,
|
|
||||||
} from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldPhone = (
|
|
||||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
|
||||||
): field is ViewFieldDefinition<ViewFieldPhoneMetadata> =>
|
|
||||||
field.metadata.type === 'phone';
|
|
||||||
@ -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';
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import {
|
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
ViewFieldProbabilityMetadata,
|
|
||||||
} from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldProbability = (
|
|
||||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
|
||||||
): field is ViewFieldDefinition<ViewFieldProbabilityMetadata> =>
|
|
||||||
field.metadata.type === 'probability';
|
|
||||||
@ -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';
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import {
|
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
ViewFieldRelationMetadata,
|
|
||||||
} from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldRelation = (
|
|
||||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
|
||||||
): field is ViewFieldDefinition<ViewFieldRelationMetadata> =>
|
|
||||||
field.metadata.type === 'relation';
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { ViewFieldRelationValue } from '../ViewField';
|
|
||||||
|
|
||||||
// TODO: add yup
|
|
||||||
export const isViewFieldRelationValue = (
|
|
||||||
fieldValue: unknown,
|
|
||||||
): fieldValue is ViewFieldRelationValue =>
|
|
||||||
fieldValue !== undefined && typeof fieldValue === 'object';
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import {
|
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
ViewFieldTextMetadata,
|
|
||||||
} from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldText = (
|
|
||||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
|
||||||
): field is ViewFieldDefinition<ViewFieldTextMetadata> =>
|
|
||||||
field.metadata.type === 'text';
|
|
||||||
@ -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';
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import {
|
|
||||||
ViewFieldDefinition,
|
|
||||||
ViewFieldMetadata,
|
|
||||||
ViewFieldURLMetadata,
|
|
||||||
} from '../ViewField';
|
|
||||||
|
|
||||||
export const isViewFieldURL = (
|
|
||||||
field: ViewFieldDefinition<ViewFieldMetadata>,
|
|
||||||
): field is ViewFieldDefinition<ViewFieldURLMetadata> =>
|
|
||||||
field.metadata.type === 'url';
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { DateDisplay } from '@/ui/content-display/components/DateDisplay';
|
import { InlineCellContainer } from '@/ui/editable-field/components/InlineCellContainer';
|
||||||
import { EditableField } from '@/ui/editable-field/components/EditableField';
|
|
||||||
import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
|
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 { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||||
import { parseDate } from '~/utils/date-utils';
|
import { parseDate } from '~/utils/date-utils';
|
||||||
@ -30,7 +30,7 @@ export const DateEditableField = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
|
<RecoilScope CustomRecoilScopeContext={FieldRecoilScopeContext}>
|
||||||
<EditableField
|
<InlineCellContainer
|
||||||
IconLabel={Icon}
|
IconLabel={Icon}
|
||||||
label={label}
|
label={label}
|
||||||
editModeContent={
|
editModeContent={
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { DateInput } from '@/ui/input/components/DateInput';
|
|||||||
import { Nullable } from '~/types/Nullable';
|
import { Nullable } from '~/types/Nullable';
|
||||||
import { parseDate } from '~/utils/date-utils';
|
import { parseDate } from '~/utils/date-utils';
|
||||||
|
|
||||||
import { useEditableField } from '../../hooks/useEditableField';
|
import { useInlineCell } from '../../hooks/useInlineCell';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
value: string;
|
value: string;
|
||||||
@ -24,7 +24,7 @@ export const EditableFieldEditModeDate = ({
|
|||||||
setInternalValue(value);
|
setInternalValue(value);
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const { closeEditableField } = useEditableField();
|
const { closeInlineCell: closeEditableField } = useInlineCell();
|
||||||
|
|
||||||
const handleClickOutside = () => {
|
const handleClickOutside = () => {
|
||||||
closeEditableField();
|
closeEditableField();
|
||||||
|
|||||||
59
front/src/modules/ui/field/components/FieldDisplay.tsx
Normal file
59
front/src/modules/ui/field/components/FieldDisplay.tsx
Normal 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 />
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
150
front/src/modules/ui/field/components/FieldInput.tsx
Normal file
150
front/src/modules/ui/field/components/FieldInput.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
17
front/src/modules/ui/field/contexts/FieldContext.ts
Normal file
17
front/src/modules/ui/field/contexts/FieldContext.ts
Normal 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,
|
||||||
|
);
|
||||||
23
front/src/modules/ui/field/hooks/useIsFieldEmpty.ts
Normal file
23
front/src/modules/ui/field/hooks/useIsFieldEmpty.ts
Normal 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;
|
||||||
|
};
|
||||||
15
front/src/modules/ui/field/hooks/useIsFieldInputOnly.ts
Normal file
15
front/src/modules/ui/field/hooks/useIsFieldInputOnly.ts
Normal 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;
|
||||||
|
};
|
||||||
184
front/src/modules/ui/field/hooks/usePersistField.ts
Normal file
184
front/src/modules/ui/field/hooks/usePersistField.ts
Normal 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;
|
||||||
|
};
|
||||||
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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
Reference in New Issue
Block a user