Feat/generic editable cell all types (#987)

* Added generic relation cell

* Deactivated debug

* Added default warning

* Put back display component

* Removed unused types

* wip

* Renamed to view field

* Use new view field structure to have chip working

* Finished

* Added a temp feature flag

* Added double text chip cell

* Ok

* Finished tables

* Fixed icon size

* Fixed bug on date field

* Use icon index

* Fix

* Fixed naming

* Fix

* removed file from merge

* Fixed tests

* Coverage
This commit is contained in:
Lucas Bordeau
2023-07-29 23:48:43 +02:00
committed by GitHub
parent dc18bc40b0
commit d9f6ae8663
77 changed files with 1730 additions and 326 deletions

View File

@ -167,8 +167,8 @@
"workerDirectory": "public"
},
"nyc": {
"statements": 70,
"lines": 70,
"statements": 65,
"lines": 65,
"functions": 60,
"exclude": [
"src/generated/**/*"

View File

@ -20,7 +20,7 @@ import { AppInternalHooks } from '~/sync-hooks/AppInternalHooks';
import { SignInUp } from './pages/auth/SignInUp';
// TEMP FEATURE FLAG FOR VIEW FIELDS
export const ACTIVATE_VIEW_FIELDS = false;
export const ACTIVATE_VIEW_FIELDS = true;
export function App() {
return (

View File

@ -1,22 +0,0 @@
import { IconBuildingSkyscraper } from '@tabler/icons-react';
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
import {
ViewFieldChipMetadata,
ViewFieldDefinition,
} from '@/ui/table/types/ViewField';
export const companyViewFields: ViewFieldDefinition<unknown>[] = [
{
columnLabel: 'Name',
columnIcon: <IconBuildingSkyscraper size={16} />,
columnSize: 150,
type: 'chip',
columnOrder: 1,
metadata: {
urlFieldName: 'domainName',
contentFieldName: 'name',
relationType: Entity.Company,
},
} as ViewFieldDefinition<ViewFieldChipMetadata>,
];

View File

@ -0,0 +1,105 @@
import {
IconBuildingSkyscraper,
IconCalendarEvent,
IconLink,
IconMap,
IconUser,
IconUsers,
} from '@/ui/icon/index';
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
import {
ViewFieldChipMetadata,
ViewFieldDateMetadata,
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldNumberMetadata,
ViewFieldRelationMetadata,
ViewFieldTextMetadata,
ViewFieldURLMetadata,
} from '@/ui/table/types/ViewField';
export const companyViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
{
id: 'name',
columnLabel: 'Name',
columnIcon: <IconBuildingSkyscraper />,
columnSize: 180,
columnOrder: 1,
metadata: {
type: 'chip',
urlFieldName: 'domainName',
contentFieldName: 'name',
relationType: Entity.Company,
},
} as ViewFieldDefinition<ViewFieldChipMetadata>,
{
id: 'domainName',
columnLabel: 'URL',
columnIcon: <IconLink />,
columnSize: 100,
columnOrder: 2,
metadata: {
type: 'url',
fieldName: 'domainName',
placeHolder: 'example.com',
},
} as ViewFieldDefinition<ViewFieldURLMetadata>,
{
id: 'accountOwner',
columnLabel: 'Account Owner',
columnIcon: <IconUser />,
columnSize: 150,
columnOrder: 3,
metadata: {
type: 'relation',
fieldName: 'accountOwner',
relationType: Entity.User,
},
} satisfies ViewFieldDefinition<ViewFieldRelationMetadata>,
{
id: 'createdAt',
columnLabel: 'Creation',
columnIcon: <IconCalendarEvent />,
columnSize: 150,
columnOrder: 4,
metadata: {
type: 'date',
fieldName: 'createdAt',
},
} satisfies ViewFieldDefinition<ViewFieldDateMetadata>,
{
id: 'employees',
columnLabel: 'Employees',
columnIcon: <IconUsers />,
columnSize: 150,
columnOrder: 5,
metadata: {
type: 'number',
fieldName: 'employees',
},
} satisfies ViewFieldDefinition<ViewFieldNumberMetadata>,
{
id: 'linkedin',
columnLabel: 'LinkedIn',
columnIcon: <IconMap />,
columnSize: 170,
columnOrder: 6,
metadata: {
type: 'url',
fieldName: 'linkedinUrl',
placeHolder: 'LinkedIn URL',
},
} satisfies ViewFieldDefinition<ViewFieldURLMetadata>,
{
id: 'address',
columnLabel: 'Address',
columnIcon: <IconMap />,
columnSize: 170,
columnOrder: 7,
metadata: {
type: 'text',
fieldName: 'address',
placeHolder: 'Address',
},
} satisfies ViewFieldDefinition<ViewFieldTextMetadata>,
];

View File

@ -22,8 +22,20 @@ export function CompanyCreatedAtEditableField({ company }: OwnProps) {
setInternalValue(company.createdAt);
}, [company.createdAt]);
// TODO: refactor change and submit
async function handleChange(newValue: string) {
setInternalValue(newValue);
await updateCompany({
variables: {
where: {
id: company.id,
},
data: {
createdAt: newValue ?? '',
},
},
});
}
async function handleSubmit() {

View File

@ -1,14 +1,14 @@
import { useCallback, useMemo, useState } from 'react';
import { companyViewFields } from '@/companies/constants/companyFieldMetadataArray';
import { companyViewFields } from '@/companies/constants/companyViewFields';
import { CompaniesSelectedSortType, defaultOrderBy } from '@/companies/queries';
import { GenericEntityTableData } from '@/people/components/GenericEntityTableData';
import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
import { IconList } from '@/ui/icon';
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue';
import { EntityTable } from '@/ui/table/components/EntityTableV2';
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
import { TableContext } from '@/ui/table/states/TableContext';
import {
CompanyOrderByWithRelationInput,

View File

@ -1,52 +0,0 @@
import {
IconBriefcase,
IconBuildingSkyscraper,
IconMap,
} from '@tabler/icons-react';
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
import {
ViewFieldDefinition,
ViewFieldRelationMetadata,
ViewFieldTextMetadata,
} from '@/ui/table/types/ViewField';
export const peopleViewFields: ViewFieldDefinition<unknown>[] = [
{
id: 'city',
columnLabel: 'City',
columnIcon: <IconMap size={16} />,
columnSize: 150,
type: 'text',
columnOrder: 1,
metadata: {
fieldName: 'city',
placeHolder: 'City',
},
} as ViewFieldDefinition<ViewFieldTextMetadata>,
{
id: 'jobTitle',
columnLabel: 'Job title',
columnIcon: <IconBriefcase size={16} />,
columnSize: 150,
type: 'text',
columnOrder: 2,
metadata: {
fieldName: 'jobTitle',
placeHolder: 'Job title',
},
} as ViewFieldDefinition<ViewFieldTextMetadata>,
{
id: 'company',
columnLabel: 'Company',
columnIcon: <IconBuildingSkyscraper size={16} />,
columnSize: 150,
type: 'relation',
relationType: Entity.Company,
columnOrder: 3,
metadata: {
fieldName: 'company',
relationType: Entity.Company,
},
} as ViewFieldDefinition<ViewFieldRelationMetadata>,
];

View File

@ -0,0 +1,122 @@
import {
IconBrandLinkedin,
IconBriefcase,
IconBuildingSkyscraper,
IconCalendarEvent,
IconMail,
IconMap,
IconPhone,
IconUser,
} from '@/ui/icon/index';
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
import {
ViewFieldDateMetadata,
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
ViewFieldMetadata,
ViewFieldPhoneMetadata,
ViewFieldRelationMetadata,
ViewFieldTextMetadata,
ViewFieldURLMetadata,
} from '@/ui/table/types/ViewField';
export const peopleViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
{
id: 'displayName',
columnLabel: 'People',
columnIcon: <IconUser />,
columnSize: 210,
columnOrder: 1,
metadata: {
type: 'double-text-chip',
firstValueFieldName: 'firstName',
secondValueFieldName: 'lastName',
firstValuePlaceholder: 'First name',
secondValuePlaceholder: 'Last name',
entityType: Entity.Person,
},
} satisfies ViewFieldDefinition<ViewFieldDoubleTextChipMetadata>,
{
id: 'email',
columnLabel: 'Email',
columnIcon: <IconMail />,
columnSize: 150,
columnOrder: 2,
metadata: {
type: 'text',
fieldName: 'email',
placeHolder: 'Email',
},
} satisfies ViewFieldDefinition<ViewFieldTextMetadata>,
{
id: 'company',
columnLabel: 'Company',
columnIcon: <IconBuildingSkyscraper />,
columnSize: 150,
columnOrder: 3,
metadata: {
type: 'relation',
fieldName: 'company',
relationType: Entity.Company,
},
} satisfies ViewFieldDefinition<ViewFieldRelationMetadata>,
{
id: 'phone',
columnLabel: 'Phone',
columnIcon: <IconPhone />,
columnSize: 150,
columnOrder: 4,
metadata: {
type: 'phone',
fieldName: 'phone',
placeHolder: 'Phone',
},
} satisfies ViewFieldDefinition<ViewFieldPhoneMetadata>,
{
id: 'createdAt',
columnLabel: 'Creation',
columnIcon: <IconCalendarEvent />,
columnSize: 150,
columnOrder: 5,
metadata: {
type: 'date',
fieldName: 'createdAt',
},
} satisfies ViewFieldDefinition<ViewFieldDateMetadata>,
{
id: 'city',
columnLabel: 'City',
columnIcon: <IconMap />,
columnSize: 150,
columnOrder: 6,
metadata: {
type: 'text',
fieldName: 'city',
placeHolder: 'City',
},
} satisfies ViewFieldDefinition<ViewFieldTextMetadata>,
{
id: 'jobTitle',
columnLabel: 'Job title',
columnIcon: <IconBriefcase />,
columnSize: 150,
columnOrder: 7,
metadata: {
type: 'text',
fieldName: 'jobTitle',
placeHolder: 'Job title',
},
} satisfies ViewFieldDefinition<ViewFieldTextMetadata>,
{
id: 'linkedin',
columnLabel: 'LinkedIn',
columnIcon: <IconBrandLinkedin />,
columnSize: 150,
columnOrder: 8,
metadata: {
type: 'url',
fieldName: 'linkedinUrl',
placeHolder: 'LinkedIn',
},
} satisfies ViewFieldDefinition<ViewFieldURLMetadata>,
];

View File

@ -1,80 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext';
import { viewFieldsState } from '@/ui/table/states/viewFieldsState';
import { isViewFieldChip } from '@/ui/table/types/guards/isViewFieldChip';
import { isViewFieldRelation } from '@/ui/table/types/guards/isViewFieldRelation';
import { isViewFieldText } from '@/ui/table/types/guards/isViewFieldText';
export function useUpdateEntityField() {
const useUpdateEntityMutation = useContext(EntityUpdateMutationHookContext);
const [updateEntity] = useUpdateEntityMutation();
const viewFields = useRecoilValue(viewFieldsState);
return function updatePeopleField(
currentEntityId: string,
viewFieldId: string,
newFieldValue: unknown,
) {
const viewField = viewFields.find(
(metadata) => metadata.id === viewFieldId,
);
if (!viewField) {
throw new Error(`View field not found for id ${viewFieldId}`);
}
// TODO: improve type narrowing here with validation maybe ? Also validate the newFieldValue with linked type guards
if (isViewFieldRelation(viewField)) {
const newSelectedEntity = newFieldValue as EntityForSelect | null;
const fieldName = viewField.metadata.fieldName;
if (!newSelectedEntity) {
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[fieldName]: {
disconnect: true,
},
},
},
});
} else {
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[fieldName]: {
connect: { id: newSelectedEntity.id },
},
},
},
});
}
} else if (isViewFieldChip(viewField)) {
const newContent = newFieldValue as string;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.contentFieldName]: newContent },
},
});
} else if (isViewFieldText(viewField)) {
const newContent = newFieldValue as string;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
}
};
}

View File

@ -1,8 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import { defaultOrderBy } from '@/companies/queries';
import { GenericEntityTableData } from '@/people/components/GenericEntityTableData';
import { peopleViewFields } from '@/people/constants/peopleFieldMetadataArray';
import { peopleViewFields } from '@/people/constants/peopleViewFields';
import { PeopleSelectedSortType } from '@/people/queries';
import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
@ -10,6 +9,7 @@ import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIn
import { IconList } from '@/ui/icon';
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue';
import { EntityTable } from '@/ui/table/components/EntityTableV2';
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
import { TableContext } from '@/ui/table/states/TableContext';
import {
PersonOrderByWithRelationInput,

View File

@ -1,7 +1,7 @@
import { ChangeEvent, useMemo, useState } from 'react';
import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode';
import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
import { debounce } from '~/utils/debounce';
import { BoardCardEditableField } from './BoardCardEditableField';

View File

@ -1,7 +1,7 @@
import { formatToHumanReadableDate } from '~/utils';
type OwnProps = {
value: Date | null;
value: Date | string | null;
};
export function InplaceInputDateDisplayMode({ value }: OwnProps) {

View File

@ -0,0 +1,36 @@
import { MouseEvent } from 'react';
import styled from '@emotion/styled';
import { RawLink } from '@/ui/link/components/RawLink';
const StyledRawLink = styled(RawLink)`
overflow: hidden;
a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
type OwnProps = {
value: string;
};
export function InplaceInputURLDisplayMode({ value }: OwnProps) {
function handleClick(event: MouseEvent<HTMLElement>) {
event.stopPropagation();
}
const absoluteUrl = value
? value.startsWith('http')
? value
: 'https://' + value
: '';
return (
<StyledRawLink href={absoluteUrl} onClick={handleClick}>
{value}
</StyledRawLink>
);
}

View File

@ -3,7 +3,7 @@ import { useRecoilCallback } from 'recoil';
import { internalHotkeysEnabledScopesState } from '../states/internal/internalHotkeysEnabledScopesState';
const DEBUG_HOTKEY_SCOPE = false;
const DEBUG_HOTKEY_SCOPE = true;
export function useScopedHotkeyCallback() {
return useRecoilCallback(

View File

@ -1,7 +1,7 @@
import { ChangeEvent } from 'react';
import styled from '@emotion/styled';
import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
type OwnProps = {
firstValue: string;
@ -11,7 +11,7 @@ type OwnProps = {
onChange: (firstValue: string, secondValue: string) => void;
};
const StyledContainer = styled.div`
export const StyledDoubleTextContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
@ -30,7 +30,7 @@ export function InplaceInputDoubleText({
onChange,
}: OwnProps) {
return (
<StyledContainer>
<StyledDoubleTextContainer>
<StyledInput
autoFocus
placeholder={firstValuePlaceholder}
@ -47,6 +47,6 @@ export function InplaceInputDoubleText({
onChange(firstValue, event.target.value);
}}
/>
</StyledContainer>
</StyledDoubleTextContainer>
);
}

View File

@ -0,0 +1,79 @@
import { ChangeEvent, useEffect, useRef, useState } from 'react';
import styled from '@emotion/styled';
import { textInputStyle } from '@/ui/themes/effects';
import { useRegisterCloseCellHandlers } from '../../table/editable-cell/hooks/useRegisterCloseCellHandlers';
import { StyledDoubleTextContainer } from './InplaceInputDoubleText';
export const StyledInput = styled.input`
margin: 0;
width: 100%;
${textInputStyle}
`;
type OwnProps = {
firstValuePlaceholder?: string;
secondValuePlaceholder?: string;
firstValue: string;
secondValue: string;
onSubmit: (newFirstValue: string, newSecondValue: string) => void;
};
export function InplaceInputDoubleTextCellEditMode({
firstValue,
secondValue,
firstValuePlaceholder,
secondValuePlaceholder,
onSubmit,
}: OwnProps) {
const [internalFirstValue, setInternalFirstValue] = useState(firstValue);
const [internalSecondValue, setInternalSecondValue] = useState(secondValue);
const wrapperRef = useRef(null);
function handleSubmit() {
onSubmit(internalFirstValue, internalSecondValue);
}
function handleCancel() {
setInternalFirstValue(firstValue);
setInternalSecondValue(secondValue);
}
function handleFirstValueChange(event: ChangeEvent<HTMLInputElement>) {
setInternalFirstValue(event.target.value);
}
function handleSecondValueChange(event: ChangeEvent<HTMLInputElement>) {
setInternalSecondValue(event.target.value);
}
useEffect(() => {
setInternalFirstValue(firstValue);
}, [firstValue]);
useEffect(() => {
setInternalSecondValue(secondValue);
}, [secondValue]);
useRegisterCloseCellHandlers(wrapperRef, handleSubmit, handleCancel);
return (
<StyledDoubleTextContainer ref={wrapperRef}>
<StyledInput
autoFocus
placeholder={firstValuePlaceholder}
value={internalFirstValue}
onChange={handleFirstValueChange}
/>
<StyledInput
autoComplete="off"
placeholder={secondValuePlaceholder}
value={internalSecondValue}
onChange={handleSecondValueChange}
/>
</StyledDoubleTextContainer>
);
}

View File

@ -18,7 +18,7 @@ type OwnProps = {
onSubmit: (newText: string) => void;
};
export function InplaceInputTextEditMode({
export function InplaceInputTextCellEditMode({
placeholder,
autoFocus,
value,

View File

@ -11,13 +11,18 @@ const StyledTitle = styled.div`
display: flex;
flex-direction: row;
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(8)};
padding-left: ${({ theme }) => theme.spacing(2)};
`;
const StyledIcon = styled.div`
display: flex;
margin-right: ${({ theme }) => theme.spacing(1)};
& > svg {
height: ${({ theme }) => theme.icon.size.md}px;
width: ${({ theme }) => theme.icon.size.md}px;
}
`;
export function ColumnHead({ viewName, viewIcon }: OwnProps) {

View File

@ -25,9 +25,9 @@ export function EntityTableCell({ cellIndex }: { cellIndex: number }) {
});
}
const entityFieldMetadata = useContext(ViewFieldContext);
const viewField = useContext(ViewFieldContext);
if (!entityFieldMetadata) {
if (!viewField) {
return null;
}
@ -37,12 +37,12 @@ export function EntityTableCell({ cellIndex }: { cellIndex: number }) {
<td
onContextMenu={(event) => handleContextMenu(event)}
style={{
width: entityFieldMetadata.columnSize,
minWidth: entityFieldMetadata.columnSize,
maxWidth: entityFieldMetadata.columnSize,
width: viewField.columnSize,
minWidth: viewField.columnSize,
maxWidth: viewField.columnSize,
}}
>
<GenericEditableCell fieldDefinition={entityFieldMetadata} />
<GenericEditableCell viewField={viewField} />
</td>
</ColumnIndexContext.Provider>
</RecoilScope>

View File

@ -1,12 +1,12 @@
import { useRecoilValue } from 'recoil';
import { viewFieldsState } from '../states/viewFieldsState';
import { viewFieldsFamilyState } from '../states/viewFieldsState';
import { ColumnHead } from './ColumnHead';
import { SelectAllCheckbox } from './SelectAllCheckbox';
export function EntityTableHeader() {
const viewFields = useRecoilValue(viewFieldsState);
const viewFields = useRecoilValue(viewFieldsFamilyState);
return (
<thead>

View File

@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { ViewFieldContext } from '../states/ViewFieldContext';
import { viewFieldsState } from '../states/viewFieldsState';
import { viewFieldsFamilyState } from '../states/viewFieldsState';
import { CheckboxCell } from './CheckboxCell';
import { EntityTableCell } from './EntityTableCellV2';
@ -13,18 +13,18 @@ const StyledRow = styled.tr<{ selected: boolean }>`
`;
export function EntityTableRow({ rowId }: { rowId: string }) {
const entityFieldMetadataArray = useRecoilValue(viewFieldsState);
const viewFields = useRecoilValue(viewFieldsFamilyState);
return (
<StyledRow data-testid={`row-id-${rowId}`} selected={false}>
<td>
<CheckboxCell />
</td>
{entityFieldMetadataArray.map((entityFieldMetadata, columnIndex) => {
{viewFields.map((viewField, columnIndex) => {
return (
<ViewFieldContext.Provider
value={entityFieldMetadata}
key={entityFieldMetadata.columnOrder}
value={viewField}
key={viewField.columnOrder}
>
<EntityTableCell cellIndex={columnIndex} />
</ViewFieldContext.Provider>

View File

@ -1,37 +1,54 @@
import { ViewFieldDefinition } from '@/ui/table/types/ViewField';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/table/types/ViewField';
import { isViewFieldChip } from '../types/guards/isViewFieldChip';
import { isViewFieldDate } from '../types/guards/isViewFieldDate';
import { isViewFieldDoubleText } from '../types/guards/isViewFieldDoubleText';
import { isViewFieldDoubleTextChip } from '../types/guards/isViewFieldDoubleTextChip';
import { isViewFieldNumber } from '../types/guards/isViewFieldNumber';
import { isViewFieldPhone } from '../types/guards/isViewFieldPhone';
import { isViewFieldRelation } from '../types/guards/isViewFieldRelation';
import { isViewFieldText } from '../types/guards/isViewFieldText';
import { isViewFieldURL } from '../types/guards/isViewFieldURL';
import { GenericEditableChipCell } from './GenericEditableChipCell';
import { GenericEditableDateCell } from './GenericEditableDateCell';
import { GenericEditableDoubleTextCell } from './GenericEditableDoubleTextCell';
import { GenericEditableDoubleTextChipCell } from './GenericEditableDoubleTextChipCell';
import { GenericEditableNumberCell } from './GenericEditableNumberCell';
import { GenericEditablePhoneCell } from './GenericEditablePhoneCell';
import { GenericEditableRelationCell } from './GenericEditableRelationCell';
import { GenericEditableTextCell } from './GenericEditableTextCell';
import { GenericEditableURLCell } from './GenericEditableURLCell';
type OwnProps = {
fieldDefinition: ViewFieldDefinition<unknown>;
viewField: ViewFieldDefinition<ViewFieldMetadata>;
};
export function GenericEditableCell({ fieldDefinition }: OwnProps) {
export function GenericEditableCell({ viewField: fieldDefinition }: OwnProps) {
if (isViewFieldText(fieldDefinition)) {
return (
<GenericEditableTextCell
viewField={fieldDefinition}
editModeHorizontalAlign="left"
/>
);
return <GenericEditableTextCell viewField={fieldDefinition} />;
} else if (isViewFieldRelation(fieldDefinition)) {
return <GenericEditableRelationCell fieldDefinition={fieldDefinition} />;
} else if (isViewFieldDoubleTextChip(fieldDefinition)) {
return <GenericEditableDoubleTextChipCell viewField={fieldDefinition} />;
} else if (isViewFieldDoubleText(fieldDefinition)) {
return <GenericEditableDoubleTextCell viewField={fieldDefinition} />;
} else if (isViewFieldPhone(fieldDefinition)) {
return <GenericEditablePhoneCell viewField={fieldDefinition} />;
} else if (isViewFieldURL(fieldDefinition)) {
return <GenericEditableURLCell viewField={fieldDefinition} />;
} else if (isViewFieldDate(fieldDefinition)) {
return <GenericEditableDateCell viewField={fieldDefinition} />;
} else if (isViewFieldNumber(fieldDefinition)) {
return <GenericEditableNumberCell viewField={fieldDefinition} />;
} else if (isViewFieldChip(fieldDefinition)) {
return (
<GenericEditableChipCell
viewField={fieldDefinition}
editModeHorizontalAlign="left"
/>
);
return <GenericEditableChipCell viewField={fieldDefinition} />;
} else {
console.warn(
`Unknown field type: ${fieldDefinition.type} in GenericEditableCell`,
`Unknown field metadata type: ${fieldDefinition.metadata.type} in GenericEditableCell`,
);
return <></>;
}

View File

@ -3,7 +3,7 @@ import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { ViewFieldChipMetadata, ViewFieldDefinition } from '../types/ViewField';
import { GenericEditableChipCellDisplayMode } from './GenericEditableChipCellDisplayMode';
import { GenericEditableTextCellEditMode } from './GenericEditableTextCellEditMode';
import { GenericEditableChipCellEditMode } from './GenericEditableChipCellEditMode';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldChipMetadata>;
@ -14,17 +14,12 @@ type OwnProps = {
export function GenericEditableChipCell({
viewField,
editModeHorizontalAlign,
placeholder,
}: OwnProps) {
return (
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<GenericEditableTextCellEditMode
fieldName={viewField.metadata.contentFieldName}
viewFieldId={viewField.id}
placeholder={placeholder}
/>
<GenericEditableChipCellEditMode viewField={viewField} />
}
nonEditModeContent={
<GenericEditableChipCellDisplayMode fieldDefinition={viewField} />

View File

@ -0,0 +1,45 @@
import { useRecoilState } from 'recoil';
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { ViewFieldChipMetadata, ViewFieldDefinition } from '../types/ViewField';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldChipMetadata>;
};
export function GenericEditableChipCellEditMode({ viewField }: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.contentFieldName,
}),
);
const updateField = useUpdateEntityField();
function handleSubmit(newText: string) {
if (newText === fieldValue) return;
setFieldValue(newText);
if (currentRowEntityId && updateField) {
updateField(currentRowEntityId, viewField, newText);
}
}
return (
<InplaceInputTextCellEditMode
placeholder={viewField.metadata.placeHolder ?? ''}
autoFocus
value={fieldValue ?? ''}
onSubmit={handleSubmit}
/>
);
}

View File

@ -0,0 +1,39 @@
import { useRecoilValue } from 'recoil';
import { InplaceInputDateDisplayMode } from '@/ui/display/component/InplaceInputDateDisplayMode';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { ViewFieldDateMetadata, ViewFieldDefinition } from '../types/ViewField';
import { GenericEditableDateCellEditMode } from './GenericEditableDateCellEditMode';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldDateMetadata>;
editModeHorizontalAlign?: 'left' | 'right';
};
export function GenericEditableDateCell({
viewField,
editModeHorizontalAlign,
}: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
const fieldValue = useRecoilValue<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
return (
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<GenericEditableDateCellEditMode viewField={viewField} />
}
nonEditModeContent={<InplaceInputDateDisplayMode value={fieldValue} />}
></EditableCell>
);
}

View File

@ -0,0 +1,50 @@
import { DateTime } from 'luxon';
import { useRecoilState } from 'recoil';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { EditableCellDateEditMode } from '../editable-cell/types/EditableCellDateEditMode';
import { ViewFieldDateMetadata, ViewFieldDefinition } from '../types/ViewField';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldDateMetadata>;
};
export function GenericEditableDateCellEditMode({ viewField }: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
const updateField = useUpdateEntityField();
function handleSubmit(newDate: Date) {
const fieldValueDate = fieldValue
? DateTime.fromISO(fieldValue).toJSDate()
: null;
const newDateISO = DateTime.fromJSDate(newDate).toISO();
if (newDate === fieldValueDate || !newDateISO) return;
setFieldValue(newDateISO);
if (currentRowEntityId && updateField && newDateISO) {
updateField(currentRowEntityId, viewField, newDateISO);
}
}
return (
<EditableCellDateEditMode
value={DateTime.fromISO(fieldValue).toJSDate()}
onSubmit={handleSubmit}
/>
);
}

View File

@ -0,0 +1,48 @@
import { useRecoilValue } from 'recoil';
import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldDoubleTextMetadata,
} from '../types/ViewField';
import { GenericEditableDoubleTextCellEditMode } from './GenericEditableDoubleTextCellEditMode';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldDoubleTextMetadata>;
};
export function GenericEditableDoubleTextCell({ viewField }: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
const firstValue = useRecoilValue<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.firstValueFieldName,
}),
);
const secondValue = useRecoilValue<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.secondValueFieldName,
}),
);
const displayName = `${firstValue ?? ''} ${secondValue ?? ''}`;
return (
<EditableCell
editModeContent={
<GenericEditableDoubleTextCellEditMode viewField={viewField} />
}
nonEditModeContent={
<InplaceInputTextDisplayMode>{displayName}</InplaceInputTextDisplayMode>
}
></EditableCell>
);
}

View File

@ -0,0 +1,60 @@
import { useRecoilState } from 'recoil';
import { InplaceInputDoubleTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputDoubleTextCellEditMode';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldDoubleTextMetadata,
} from '../types/ViewField';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldDoubleTextMetadata>;
};
export function GenericEditableDoubleTextCellEditMode({ viewField }: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
// TODO: we could use a hook that would return the field value with the right type
const [firstValue, setFirstValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.firstValueFieldName,
}),
);
const [secondValue, setSecondValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.firstValueFieldName,
}),
);
const updateField = useUpdateEntityField();
function handleSubmit(newFirstValue: string, newSecondValue: string) {
if (newFirstValue === firstValue && newSecondValue === secondValue) return;
setFirstValue(newFirstValue);
setSecondValue(newSecondValue);
if (currentRowEntityId && updateField) {
updateField(currentRowEntityId, viewField, {
firstValue: newFirstValue,
secondValue: newSecondValue,
});
}
}
return (
<InplaceInputDoubleTextCellEditMode
firstValuePlaceholder={viewField.metadata.firstValuePlaceholder}
secondValuePlaceholder={viewField.metadata.secondValuePlaceholder}
firstValue={firstValue ?? ''}
secondValue={secondValue ?? ''}
onSubmit={handleSubmit}
/>
);
}

View File

@ -0,0 +1,28 @@
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { TableHotkeyScope } from '../types/TableHotkeyScope';
import {
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
} from '../types/ViewField';
import { GenericEditableDoubleTextChipCellDisplayMode } from './GenericEditableDoubleTextChipCellDisplayMode';
import { GenericEditableDoubleTextChipCellEditMode } from './GenericEditableDoubleTextChipCellEditMode';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldDoubleTextChipMetadata>;
};
export function GenericEditableDoubleTextChipCell({ viewField }: OwnProps) {
return (
<EditableCell
editHotkeyScope={{ scope: TableHotkeyScope.CellDoubleTextInput }}
editModeContent={
<GenericEditableDoubleTextChipCellEditMode viewField={viewField} />
}
nonEditModeContent={
<GenericEditableDoubleTextChipCellDisplayMode viewField={viewField} />
}
></EditableCell>
);
}

View File

@ -0,0 +1,51 @@
import { useRecoilState } from 'recoil';
import { CompanyChip } from '@/companies/components/CompanyChip';
import { PersonChip } from '@/people/components/PersonChip';
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
} from '@/ui/table/types/ViewField';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldDoubleTextChipMetadata>;
};
export function GenericEditableDoubleTextChipCellDisplayMode({
viewField,
}: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
const [firstValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.firstValueFieldName,
}),
);
const [secondValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.secondValueFieldName,
}),
);
const displayName = `${firstValue} ${secondValue}`;
switch (viewField.metadata.entityType) {
case Entity.Company: {
return <CompanyChip id={currentRowEntityId ?? ''} name={displayName} />;
}
case Entity.Person: {
return <PersonChip id={currentRowEntityId ?? ''} name={displayName} />;
}
default:
console.warn(
`Unknown relation type: "${viewField.metadata.entityType}" in GenericEditableDoubleTextChipCellDisplayMode`,
);
return <> </>;
}
}

View File

@ -0,0 +1,72 @@
import { useRecoilState } from 'recoil';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { EditableCellDoubleTextEditMode } from '../editable-cell/types/EditableCellDoubleTextEditMode';
import {
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
} from '../types/ViewField';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldDoubleTextChipMetadata>;
};
export function GenericEditableDoubleTextChipCellEditMode({
viewField,
}: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
// TODO: we could use a hook that would return the field value with the right type
const [firstValue, setFirstValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.firstValueFieldName,
}),
);
const [secondValue, setSecondValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.secondValueFieldName,
}),
);
const updateField = useUpdateEntityField();
function handleSubmit(newFirstValue: string, newSecondValue: string) {
const firstValueChanged = newFirstValue !== firstValue;
const secondValueChanged = newSecondValue !== secondValue;
if (firstValueChanged) {
setFirstValue(newFirstValue);
}
if (secondValueChanged) {
setSecondValue(newSecondValue);
}
if (
currentRowEntityId &&
updateField &&
(firstValueChanged || secondValueChanged)
) {
updateField(currentRowEntityId, viewField, {
firstValue: firstValueChanged ? newFirstValue : firstValue,
secondValue: secondValueChanged ? newSecondValue : secondValue,
});
}
}
return (
<EditableCellDoubleTextEditMode
firstValuePlaceholder={viewField.metadata.firstValuePlaceholder}
secondValuePlaceholder={viewField.metadata.secondValuePlaceholder}
firstValue={firstValue ?? ''}
secondValue={secondValue ?? ''}
onSubmit={handleSubmit}
/>
);
}

View File

@ -0,0 +1,41 @@
import { useRecoilValue } from 'recoil';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldNumberMetadata,
} from '../types/ViewField';
import { GenericEditableNumberCellEditMode } from './GenericEditableNumberCellEditMode';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldNumberMetadata>;
editModeHorizontalAlign?: 'left' | 'right';
};
export function GenericEditableNumberCell({
viewField,
editModeHorizontalAlign,
}: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
const fieldValue = useRecoilValue<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
return (
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<GenericEditableNumberCellEditMode viewField={viewField} />
}
nonEditModeContent={<>{fieldValue}</>}
></EditableCell>
);
}

View File

@ -0,0 +1,66 @@
import { useRecoilState } from 'recoil';
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldNumberMetadata,
} from '../types/ViewField';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldNumberMetadata>;
};
export function GenericEditableNumberCellEditMode({ viewField }: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
const updateField = useUpdateEntityField();
function handleSubmit(newText: string) {
if (newText === fieldValue) return;
try {
const numberValue = parseInt(newText);
if (isNaN(numberValue)) {
throw new Error('Not a number');
}
// TODO: find a way to store this better in DB
if (numberValue > 2000000000) {
throw new Error('Number too big');
}
console.log({ numberValue });
setFieldValue(numberValue.toString());
if (currentRowEntityId && updateField) {
updateField(currentRowEntityId, viewField, numberValue);
}
} catch (error) {
console.warn(
`In GenericEditableNumberCellEditMode, Invalid number: ${newText}, ${error}`,
);
}
}
return (
<InplaceInputTextCellEditMode
autoFocus
value={fieldValue ?? ''}
onSubmit={handleSubmit}
/>
);
}

View File

@ -0,0 +1,42 @@
import { useRecoilValue } from 'recoil';
import { InplaceInputPhoneDisplayMode } from '@/ui/display/component/InplaceInputPhoneDisplayMode';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldPhoneMetadata,
} from '../types/ViewField';
import { GenericEditablePhoneCellEditMode } from './GenericEditablePhoneCellEditMode';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldPhoneMetadata>;
editModeHorizontalAlign?: 'left' | 'right';
};
export function GenericEditablePhoneCell({
viewField,
editModeHorizontalAlign,
}: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
const fieldValue = useRecoilValue<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
return (
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<GenericEditablePhoneCellEditMode viewField={viewField} />
}
nonEditModeContent={<InplaceInputPhoneDisplayMode value={fieldValue} />}
></EditableCell>
);
}

View File

@ -0,0 +1,48 @@
import { useRecoilState } from 'recoil';
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldPhoneMetadata,
} from '../types/ViewField';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldPhoneMetadata>;
};
export function GenericEditablePhoneCellEditMode({ viewField }: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
const updateField = useUpdateEntityField();
function handleSubmit(newText: string) {
if (newText === fieldValue) return;
setFieldValue(newText);
if (currentRowEntityId && updateField) {
updateField(currentRowEntityId, viewField, newText);
}
}
return (
<InplaceInputTextCellEditMode
placeholder={viewField.metadata.placeHolder ?? ''}
autoFocus
value={fieldValue ?? ''}
onSubmit={handleSubmit}
/>
);
}

View File

@ -24,9 +24,7 @@ export function GenericEditableRelationCell({
editModeHorizontalAlign={editModeHorizontalAlign}
editHotkeyScope={{ scope: RelationPickerHotkeyScope.RelationPicker }}
editModeContent={
<GenericEditableRelationCellEditMode
viewFieldDefinition={fieldDefinition}
/>
<GenericEditableRelationCellEditMode viewField={fieldDefinition} />
}
nonEditModeContent={
<GenericEditableRelationCellDisplayMode

View File

@ -8,6 +8,7 @@ import {
ViewFieldDefinition,
ViewFieldRelationMetadata,
} from '@/ui/table/types/ViewField';
import { UserChip } from '@/users/components/UserChip';
import { getLogoUrlFromDomainName } from '~/utils';
type OwnProps = {
@ -21,6 +22,7 @@ export function GenericEditableRelationCellDisplayMode({
}: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
// TODO: type value with generic getter
const fieldValue = useRecoilValue<any | null>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
@ -38,6 +40,14 @@ export function GenericEditableRelationCellDisplayMode({
/>
);
}
case Entity.User: {
return (
<UserChip
id={fieldValue?.id ?? ''}
name={fieldValue?.displayName ?? ''}
/>
);
}
default:
console.warn(
`Unknown relation type: "${fieldDefinition.metadata.relationType}" in GenericEditableRelationCellEditMode`,

View File

@ -1,24 +1,23 @@
import { useRecoilState } from 'recoil';
import { CompanyPickerCell } from '@/companies/components/CompanyPickerCell';
import { useUpdateEntityField } from '@/people/hooks/useUpdateEntityField';
import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
import { useEditableCell } from '@/ui/table/editable-cell/hooks/useEditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldRelationMetadata,
} from '@/ui/table/types/ViewField';
import { UserPicker } from '@/users/components/UserPicker';
type OwnProps = {
viewFieldDefinition: ViewFieldDefinition<ViewFieldRelationMetadata>;
viewField: ViewFieldDefinition<ViewFieldRelationMetadata>;
};
export function GenericEditableRelationCellEditMode({
viewFieldDefinition,
}: OwnProps) {
export function GenericEditableRelationCellEditMode({ viewField }: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
const { closeEditableCell } = useEditableCell();
@ -26,7 +25,7 @@ export function GenericEditableRelationCellEditMode({
const [fieldValueEntity] = useRecoilState<any | null>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewFieldDefinition.metadata.fieldName,
fieldName: viewField.metadata.fieldName,
}),
);
@ -38,11 +37,7 @@ export function GenericEditableRelationCellEditMode({
currentRowEntityId &&
updateEntityField
) {
updateEntityField(
currentRowEntityId,
viewFieldDefinition.id,
newFieldEntity,
);
updateEntityField(currentRowEntityId, viewField, newFieldEntity);
}
closeEditableCell();
@ -52,7 +47,7 @@ export function GenericEditableRelationCellEditMode({
closeEditableCell();
}
switch (viewFieldDefinition.metadata.relationType) {
switch (viewField.metadata.relationType) {
case Entity.Company: {
return (
<CompanyPickerCell
@ -62,9 +57,18 @@ export function GenericEditableRelationCellEditMode({
/>
);
}
case Entity.User: {
return (
<UserPicker
userId={fieldValueEntity?.id ?? null}
onSubmit={handleEntitySubmit}
onCancel={handleCancel}
/>
);
}
default:
console.warn(
`Unknown relation type: "${viewFieldDefinition.metadata.relationType}" in GenericEditableRelationCellEditMode`,
`Unknown relation type: "${viewField.metadata.relationType}" in GenericEditableRelationCellEditMode`,
);
return <></>;
}

View File

@ -12,13 +12,11 @@ import { GenericEditableTextCellEditMode } from './GenericEditableTextCellEditMo
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldTextMetadata>;
editModeHorizontalAlign?: 'left' | 'right';
placeholder?: string;
};
export function GenericEditableTextCell({
viewField,
editModeHorizontalAlign,
placeholder,
}: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
@ -33,11 +31,7 @@ export function GenericEditableTextCell({
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<GenericEditableTextCellEditMode
fieldName={viewField.metadata.fieldName}
viewFieldId={viewField.id}
placeholder={placeholder}
/>
<GenericEditableTextCellEditMode viewField={viewField} />
}
nonEditModeContent={
<InplaceInputTextDisplayMode>{fieldValue}</InplaceInputTextDisplayMode>

View File

@ -1,28 +1,24 @@
import { useRecoilState } from 'recoil';
import { useUpdateEntityField } from '@/people/hooks/useUpdateEntityField';
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { ViewFieldDefinition, ViewFieldTextMetadata } from '../types/ViewField';
type OwnProps = {
fieldName: string;
viewFieldId: string;
placeholder?: string;
viewField: ViewFieldDefinition<ViewFieldTextMetadata>;
};
export function GenericEditableTextCellEditMode({
fieldName,
viewFieldId,
placeholder,
}: OwnProps) {
export function GenericEditableTextCellEditMode({ viewField }: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName,
fieldName: viewField.metadata.fieldName,
}),
);
@ -34,13 +30,13 @@ export function GenericEditableTextCellEditMode({
setFieldValue(newText);
if (currentRowEntityId && updateField) {
updateField(currentRowEntityId, viewFieldId, newText);
updateField(currentRowEntityId, viewField, newText);
}
}
return (
<InplaceInputTextEditMode
placeholder={placeholder ?? ''}
<InplaceInputTextCellEditMode
placeholder={viewField.metadata.placeHolder ?? ''}
autoFocus
value={fieldValue ?? ''}
onSubmit={handleSubmit}

View File

@ -0,0 +1,37 @@
import { useRecoilValue } from 'recoil';
import { InplaceInputURLDisplayMode } from '@/ui/display/component/InplaceInputURLDisplayMode';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { ViewFieldDefinition, ViewFieldURLMetadata } from '../types/ViewField';
import { GenericEditableURLCellEditMode } from './GenericEditableURLCellEditMode';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldURLMetadata>;
editModeHorizontalAlign?: 'left' | 'right';
};
export function GenericEditableURLCell({
viewField,
editModeHorizontalAlign,
}: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
const fieldValue = useRecoilValue<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
return (
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={<GenericEditableURLCellEditMode viewField={viewField} />}
nonEditModeContent={<InplaceInputURLDisplayMode value={fieldValue} />}
></EditableCell>
);
}

View File

@ -0,0 +1,45 @@
import { useRecoilState } from 'recoil';
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { ViewFieldDefinition, ViewFieldURLMetadata } from '../types/ViewField';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldURLMetadata>;
};
export function GenericEditableURLCellEditMode({ viewField }: OwnProps) {
const currentRowEntityId = useCurrentRowEntityId();
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
const updateField = useUpdateEntityField();
function handleSubmit(newText: string) {
if (newText === fieldValue) return;
setFieldValue(newText);
if (currentRowEntityId && updateField) {
updateField(currentRowEntityId, viewField, newText);
}
}
return (
<InplaceInputTextCellEditMode
placeholder={viewField.metadata.placeHolder ?? ''}
autoFocus
value={fieldValue ?? ''}
onSubmit={handleSubmit}
/>
);
}

View File

@ -1,8 +1,11 @@
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { ViewFieldDefinition } from '@/ui/table/types/ViewField';
import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/table/types/ViewField';
import { useSetEntityTableData } from '../hooks/useSetEntityTableData';
import { defaultOrderBy } from '../queries';
import { defaultOrderBy } from '../../../people/queries';
export function GenericEntityTableData({
useGetRequest,
@ -16,7 +19,7 @@ export function GenericEntityTableData({
getRequestResultKey: string;
orderBy?: any;
whereFilters?: any;
viewFields: ViewFieldDefinition<unknown>[];
viewFields: ViewFieldDefinition<ViewFieldMetadata>[];
filterDefinitionArray: FilterDefinition[];
}) {
const setEntityTableData = useSetEntityTableData();

View File

@ -20,7 +20,7 @@ export function EditableCellDate({
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<EditableCellDateEditMode onChange={onChange} value={value} />
<EditableCellDateEditMode onSubmit={onChange} value={value} />
}
nonEditModeContent={<InplaceInputDateDisplayMode value={value} />}
editHotkeyScope={{ scope: TableHotkeyScope.CellDateEditMode }}

View File

@ -16,17 +16,17 @@ const EditableCellDateEditModeContainer = styled.div`
export type EditableDateProps = {
value: Date;
onChange: (date: Date) => void;
onSubmit: (date: Date) => void;
};
export function EditableCellDateEditMode({
value,
onChange,
onSubmit,
}: EditableDateProps) {
const { closeEditableCell } = useEditableCell();
function handleDateChange(newDate: Date) {
onChange(newDate);
onSubmit(newDate);
closeEditableCell();
}

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
import { StyledInput } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
import { useMoveSoftFocus } from '../../hooks/useMoveSoftFocus';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';

View File

@ -1,5 +1,5 @@
import { InplaceInputPhoneDisplayMode } from '@/ui/display/component/InplaceInputPhoneDisplayMode';
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
import { EditableCell } from '../components/EditableCell';
@ -13,7 +13,7 @@ export function EditableCellPhone({ value, placeholder, onSubmit }: OwnProps) {
return (
<EditableCell
editModeContent={
<InplaceInputTextEditMode
<InplaceInputTextCellEditMode
autoFocus
placeholder={placeholder || ''}
value={value}

View File

@ -1,5 +1,5 @@
import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode';
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
import { CellSkeleton } from '../components/CellSkeleton';
import { EditableCell } from '../components/EditableCell';
@ -23,7 +23,7 @@ export function EditableCellText({
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<InplaceInputTextEditMode
<InplaceInputTextCellEditMode
placeholder={placeholder || ''}
autoFocus
value={value}

View File

@ -1,4 +1,4 @@
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
import { InplaceInputTextCellEditMode } from '@/ui/inplace-input/components/InplaceInputTextCellEditMode';
import { RawLink } from '../../../link/components/RawLink';
import { CellSkeleton } from '../components/CellSkeleton';
@ -25,7 +25,7 @@ export function EditableCellURL({
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<InplaceInputTextEditMode
<InplaceInputTextCellEditMode
placeholder={placeholder}
autoFocus
value={url}

View File

@ -1,7 +1,7 @@
import { ReactNode, useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { InplaceInputTextEditMode } from '../../../inplace-input/components/InplaceInputTextEditMode';
import { InplaceInputTextCellEditMode } from '../../../inplace-input/components/InplaceInputTextCellEditMode';
import { EditableCell } from '../components/EditableCell';
export type EditableChipProps = {
@ -52,7 +52,7 @@ export function EditableCellChip({
<EditableCell
editModeHorizontalAlign={editModeHorizontalAlign}
editModeContent={
<InplaceInputTextEditMode
<InplaceInputTextCellEditMode
placeholder={placeholder || ''}
autoFocus
value={inputValue}

View File

@ -1,17 +1,19 @@
import { useRecoilCallback } from 'recoil';
import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState';
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { useContextScopeId } from '@/ui/recoil-scope/hooks/useContextScopeId';
import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection';
import { entityTableDimensionsState } from '@/ui/table/states/entityTableDimensionsState';
import { isFetchingEntityTableDataState } from '@/ui/table/states/isFetchingEntityTableDataState';
import { TableContext } from '@/ui/table/states/TableContext';
import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState';
import { viewFieldsState } from '@/ui/table/states/viewFieldsState';
import { ViewFieldDefinition } from '@/ui/table/types/ViewField';
import { availableFiltersScopedState } from '../../ui/filter-n-sort/states/availableFiltersScopedState';
import { useContextScopeId } from '../../ui/recoil-scope/hooks/useContextScopeId';
import { useResetTableRowSelection } from '../../ui/table/hooks/useResetTableRowSelection';
import { entityTableDimensionsState } from '../../ui/table/states/entityTableDimensionsState';
import { isFetchingEntityTableDataState } from '../../ui/table/states/isFetchingEntityTableDataState';
import { TableContext } from '../../ui/table/states/TableContext';
import { tableRowIdsState } from '../../ui/table/states/tableRowIdsState';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { viewFieldsFamilyState } from '@/ui/table/states/viewFieldsState';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/table/types/ViewField';
export function useSetEntityTableData() {
const resetTableRowSelection = useResetTableRowSelection();
@ -22,7 +24,7 @@ export function useSetEntityTableData() {
({ set, snapshot }) =>
<T extends { id: string }>(
newEntityArray: T[],
viewFields: ViewFieldDefinition<unknown>[],
viewFields: ViewFieldDefinition<ViewFieldMetadata>[],
filters: FilterDefinition[],
) => {
for (const entity of newEntityArray) {
@ -54,7 +56,7 @@ export function useSetEntityTableData() {
set(availableFiltersScopedState(tableContextScopeId), filters);
set(viewFieldsState, viewFields);
set(viewFieldsFamilyState, viewFields);
set(isFetchingEntityTableDataState, false);
},

View File

@ -0,0 +1,228 @@
import { useContext } from 'react';
import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext';
import { isViewFieldChip } from '@/ui/table/types/guards/isViewFieldChip';
import { isViewFieldRelation } from '@/ui/table/types/guards/isViewFieldRelation';
import { isViewFieldText } from '@/ui/table/types/guards/isViewFieldText';
import { isViewFieldChipValue } from '../types/guards/isViewFieldChipValue';
import { isViewFieldDate } from '../types/guards/isViewFieldDate';
import { isViewFieldDateValue } from '../types/guards/isViewFieldDateValue';
import { isViewFieldDoubleText } from '../types/guards/isViewFieldDoubleText';
import { isViewFieldDoubleTextChip } from '../types/guards/isViewFieldDoubleTextChip';
import { isViewFieldDoubleTextChipValue } from '../types/guards/isViewFieldDoubleTextChipValue';
import { isViewFieldDoubleTextValue } from '../types/guards/isViewFieldDoubleTextValue';
import { isViewFieldNumber } from '../types/guards/isViewFieldNumber';
import { isViewFieldNumberValue } from '../types/guards/isViewFieldNumberValue';
import { isViewFieldPhone } from '../types/guards/isViewFieldPhone';
import { isViewFieldPhoneValue } from '../types/guards/isViewFieldPhoneValue';
import { isViewFieldRelationValue } from '../types/guards/isViewFieldRelationValue';
import { isViewFieldTextValue } from '../types/guards/isViewFieldTextValue';
import { isViewFieldURL } from '../types/guards/isViewFieldURL';
import { isViewFieldURLValue } from '../types/guards/isViewFieldURLValue';
import {
ViewFieldChipMetadata,
ViewFieldChipValue,
ViewFieldDateMetadata,
ViewFieldDateValue,
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
ViewFieldDoubleTextChipValue,
ViewFieldDoubleTextMetadata,
ViewFieldDoubleTextValue,
ViewFieldMetadata,
ViewFieldNumberMetadata,
ViewFieldNumberValue,
ViewFieldPhoneMetadata,
ViewFieldPhoneValue,
ViewFieldRelationMetadata,
ViewFieldRelationValue,
ViewFieldTextMetadata,
ViewFieldTextValue,
ViewFieldURLMetadata,
ViewFieldURLValue,
} from '../types/ViewField';
export function useUpdateEntityField() {
const useUpdateEntityMutation = useContext(EntityUpdateMutationHookContext);
const [updateEntity] = useUpdateEntityMutation();
return function updatePeopleField<
MetadataType extends ViewFieldMetadata,
ValueType extends MetadataType extends ViewFieldDoubleTextMetadata
? ViewFieldDoubleTextValue
: MetadataType extends ViewFieldTextMetadata
? ViewFieldTextValue
: MetadataType extends ViewFieldPhoneMetadata
? ViewFieldPhoneValue
: MetadataType extends ViewFieldURLMetadata
? ViewFieldURLValue
: MetadataType extends ViewFieldNumberMetadata
? ViewFieldNumberValue
: MetadataType extends ViewFieldDateMetadata
? ViewFieldDateValue
: MetadataType extends ViewFieldChipMetadata
? ViewFieldChipValue
: MetadataType extends ViewFieldDoubleTextChipMetadata
? ViewFieldDoubleTextChipValue
: MetadataType extends ViewFieldRelationMetadata
? ViewFieldRelationValue
: unknown,
>(
currentEntityId: string,
viewField: ViewFieldDefinition<MetadataType>,
newFieldValue: ValueType,
) {
const newFieldValueUnknown = newFieldValue as unknown;
// TODO: improve type guards organization, maybe with a common typeguard for all view fields
// taking an object of options as parameter ?
//
// The goal would be to check that the view field value not only is valid,
// but also that it is validated against the corresponding view field type
// Relation
if (
isViewFieldRelation(viewField) &&
isViewFieldRelationValue(newFieldValueUnknown)
) {
const newSelectedEntity = newFieldValueUnknown;
const fieldName = viewField.metadata.fieldName;
if (!newSelectedEntity) {
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[fieldName]: {
disconnect: true,
},
},
},
});
} else {
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[fieldName]: {
connect: { id: newSelectedEntity.id },
},
},
},
});
}
// Chip
} else if (
isViewFieldChip(viewField) &&
isViewFieldChipValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.contentFieldName]: newContent },
},
});
// Text
} else if (
isViewFieldText(viewField) &&
isViewFieldTextValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
// Double text
} else if (
isViewFieldDoubleText(viewField) &&
isViewFieldDoubleTextValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[viewField.metadata.firstValueFieldName]: newContent.firstValue,
[viewField.metadata.secondValueFieldName]: newContent.secondValue,
},
},
});
// Double Text Chip
} else if (
isViewFieldDoubleTextChip(viewField) &&
isViewFieldDoubleTextChipValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[viewField.metadata.firstValueFieldName]: newContent.firstValue,
[viewField.metadata.secondValueFieldName]: newContent.secondValue,
},
},
});
// Phone
} else if (
isViewFieldPhone(viewField) &&
isViewFieldPhoneValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
// URL
} else if (
isViewFieldURL(viewField) &&
isViewFieldURLValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
// Number
} else if (
isViewFieldNumber(viewField) &&
isViewFieldNumberValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
// Date
} else if (
isViewFieldDate(viewField) &&
isViewFieldDateValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
}
};
}

View File

@ -1,6 +1,6 @@
import { createContext } from 'react';
import { ViewFieldDefinition } from '../types/ViewField';
import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField';
export const ViewFieldContext =
createContext<ViewFieldDefinition<unknown> | null>(null);
createContext<ViewFieldDefinition<ViewFieldMetadata> | null>(null);

View File

@ -1,8 +1,10 @@
import { atom } from 'recoil';
import { ViewFieldDefinition } from '../types/ViewField';
import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField';
export const viewFieldsState = atom<ViewFieldDefinition<unknown>[]>({
key: 'viewFieldsState',
export const viewFieldsFamilyState = atom<
ViewFieldDefinition<ViewFieldMetadata>[]
>({
key: 'viewFieldsFamilyState',
default: [],
});

View File

@ -1,36 +1,114 @@
import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
export type ViewFieldType = 'text' | 'relation' | 'chip';
export type ViewFieldType =
| 'text'
| 'relation'
| 'chip'
| 'double-text-chip'
| 'double-text'
| 'number'
| 'date'
| 'phone'
| 'url';
export type ViewFieldTextMetadata = {
type: 'text';
placeHolder: string;
fieldName: string;
};
export type ViewFieldPhoneMetadata = {
type: 'phone';
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;
};
export type ViewFieldRelationMetadata = {
type: 'relation';
relationType: Entity;
fieldName: string;
};
export type ViewFieldChipMetadata = {
type: 'chip';
relationType: Entity;
contentFieldName: string;
urlFieldName: string;
placeHolder: string;
};
export type ViewFieldDefinition<
T extends
| ViewFieldTextMetadata
| ViewFieldRelationMetadata
| ViewFieldChipMetadata
| unknown,
> = {
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;
entityType: Entity;
};
export type ViewFieldMetadata = { type: ViewFieldType } & (
| ViewFieldTextMetadata
| ViewFieldRelationMetadata
| ViewFieldChipMetadata
| ViewFieldDoubleTextChipMetadata
| ViewFieldDoubleTextMetadata
| ViewFieldPhoneMetadata
| ViewFieldURLMetadata
| ViewFieldNumberMetadata
| ViewFieldDateMetadata
);
export type ViewFieldDefinition<T extends ViewFieldMetadata | unknown> = {
id: string;
columnLabel: string;
columnSize: number;
columnOrder: number;
columnIcon?: JSX.Element;
filterIcon?: JSX.Element;
type: ViewFieldType;
metadata: T;
};
export type ViewFieldTextValue = string;
export type ViewFieldChipValue = string;
export type ViewFieldDateValue = string;
export type ViewFieldPhoneValue = string;
export type ViewFieldURLValue = string;
export type ViewFieldNumberValue = number;
export type ViewFieldDoubleTextValue = {
firstValue: string;
secondValue: string;
};
export type ViewFieldDoubleTextChipValue = {
firstValue: string;
secondValue: string;
};
export type ViewFieldRelationValue = EntityForSelect | null;

View File

@ -1,7 +1,11 @@
import { ViewFieldChipMetadata, ViewFieldDefinition } from '../ViewField';
import {
ViewFieldChipMetadata,
ViewFieldDefinition,
ViewFieldMetadata,
} from '../ViewField';
export function isViewFieldChip(
field: ViewFieldDefinition<unknown>,
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldChipMetadata> {
return field.type === 'chip';
return field.metadata.type === 'chip';
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,11 @@
import { ViewFieldDefinition, ViewFieldRelationMetadata } from '../ViewField';
import {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldRelationMetadata,
} from '../ViewField';
export function isViewFieldRelation(
field: ViewFieldDefinition<unknown>,
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldRelationMetadata> {
return field.type === 'relation';
return field.metadata.type === 'relation';
}

View File

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

View File

@ -1,7 +1,11 @@
import { ViewFieldDefinition, ViewFieldTextMetadata } from '../ViewField';
import {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldTextMetadata,
} from '../ViewField';
export function isViewFieldText(
field: ViewFieldDefinition<unknown>,
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldTextMetadata> {
return field.type === 'text';
return field.metadata.type === 'text';
}

View File

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

View File

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

View File

@ -0,0 +1,12 @@
import { ViewFieldURLValue } from '../ViewField';
// TODO: add yup
export function isViewFieldURLValue(
fieldValue: unknown,
): fieldValue is ViewFieldURLValue {
return (
fieldValue !== null &&
fieldValue !== undefined &&
typeof fieldValue === 'string'
);
}

View File

@ -8,7 +8,7 @@ import { useSearchUserQuery } from '~/generated/graphql';
export type OwnProps = {
userId: string;
onSubmit: (newUserId: string) => void;
onSubmit: (newUser: EntityForSelect | null) => void;
onCancel?: () => void;
};
@ -23,7 +23,7 @@ export function UserPicker({ userId, onSubmit, onCancel }: OwnProps) {
const users = useFilteredSearchEntityQuery({
queryHook: useSearchUserQuery,
selectedIds: [userId],
selectedIds: userId ? [userId] : [],
searchFilter: searchFilter,
mappingFunction: (user) => ({
entityType: Entity.User,
@ -39,7 +39,7 @@ export function UserPicker({ userId, onSubmit, onCancel }: OwnProps) {
async function handleEntitySelected(
selectedUser: UserForSelect | null | undefined,
) {
onSubmit(selectedUser?.id ?? '');
onSubmit(selectedUser ?? null);
}
return (

View File

@ -34,11 +34,11 @@ export const FilterByName: Story = {
const filterButton = await canvas.findByText('Filter');
await userEvent.click(filterButton);
const nameFilterButton = canvas
.queryAllByTestId('dropdown-menu-item')
.find((item) => {
return item.textContent === 'Name';
});
const nameFilterButton = (
await canvas.findAllByTestId('dropdown-menu-item')
).find((item) => {
return item.textContent === 'Name';
});
assert(nameFilterButton);
@ -49,7 +49,7 @@ export const FilterByName: Story = {
delay: 200,
});
await sleep(1000);
await sleep(50);
expect(await canvas.findByText('Airbnb')).toBeInTheDocument();
expect(await canvas.findByText('Aircall')).toBeInTheDocument();
@ -88,11 +88,11 @@ export const FilterByAccountOwner: Story = {
await sleep(1000);
const charlesChip = canvas
.getAllByTestId('dropdown-menu-item')
.find((item) => {
return item.textContent?.includes('Charles Test');
});
const charlesChip = (
await canvas.findAllByTestId('dropdown-menu-item')
).find((item) => {
return item.textContent?.includes('Charles Test');
});
assert(charlesChip);

View File

@ -34,22 +34,23 @@ export const Email: Story = {
const filterButton = await canvas.findByText('Filter');
await userEvent.click(filterButton);
const emailFilterButton = canvas
.getAllByTestId('dropdown-menu-item')
.find((item) => {
return item.textContent?.includes('Email');
});
const emailFilterButton = (
await canvas.findAllByTestId('dropdown-menu-item')
).find((item) => {
return item.textContent?.includes('Email');
});
assert(emailFilterButton);
await userEvent.click(emailFilterButton);
const emailInput = canvas.getByPlaceholderText('Email');
await userEvent.type(emailInput, 'al', {
delay: 200,
});
await sleep(1000);
await sleep(50);
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument();
await expect(canvas.queryAllByText('John Doe')).toStrictEqual([]);
@ -68,11 +69,11 @@ export const CompanyName: Story = {
const filterButton = await canvas.findByText('Filter');
await userEvent.click(filterButton);
const companyFilterButton = canvas
.getAllByTestId('dropdown-menu-item')
.find((item) => {
return item.textContent?.includes('Company');
});
const companyFilterButton = (
await canvas.findAllByTestId('dropdown-menu-item')
).find((item) => {
return item.textContent?.includes('Company');
});
assert(companyFilterButton);
@ -85,11 +86,11 @@ export const CompanyName: Story = {
await sleep(500);
const qontoChip = canvas
.getAllByTestId('dropdown-menu-item')
.find((item) => {
const qontoChip = (await canvas.findAllByTestId('dropdown-menu-item')).find(
(item) => {
return item.textContent?.includes('Qonto');
});
},
);
expect(qontoChip).toBeInTheDocument();

View File

@ -191,7 +191,7 @@ export const EditRelation: Story = {
await step('Click on second row company cell', async () => {
const secondRowCompanyCell = await canvas.findByText(
mockedPeopleData[1].company.name,
mockedPeopleData[2].company.name,
);
await userEvent.click(
@ -262,11 +262,24 @@ export const SelectRelationWithKeys: Story = {
});
await userEvent.type(relationInput, '{arrowdown}');
await sleep(50);
await userEvent.type(relationInput, '{arrowup}');
await sleep(50);
await userEvent.type(relationInput, '{arrowdown}');
await sleep(50);
await userEvent.type(relationInput, '{arrowdown}');
await sleep(50);
await userEvent.type(relationInput, '{enter}');
sleep(25);
await sleep(50);
const allAirbns = await canvas.findAllByText('Aircall');
expect(allAirbns.length).toBe(1);