Feat/metadata datatable types (#2175)

* Handled new url v2 type

* Fixed refetch queries

* wip

* Ok delete but views bug

* Fix lint

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-10-21 14:07:18 +02:00
committed by GitHub
parent 598fda8f45
commit f1670f0cf4
50 changed files with 1125 additions and 350 deletions

View File

@ -12,6 +12,7 @@ import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDi
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 { URLV2FieldDisplay } from '../meta-types/display/components/URLV2FieldDisplay';
import { isFieldChip } from '../types/guards/isFieldChip';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDoubleText } from '../types/guards/isFieldDoubleText';
@ -23,6 +24,7 @@ import { isFieldPhone } from '../types/guards/isFieldPhone';
import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldURL } from '../types/guards/isFieldURL';
import { isFieldURLV2 } from '../types/guards/isFieldURLV2';
export const FieldDisplay = () => {
const { fieldDefinition } = useContext(FieldContext);
@ -43,6 +45,8 @@ export const FieldDisplay = () => {
<MoneyFieldDisplay />
) : isFieldURL(fieldDefinition) ? (
<URLFieldDisplay />
) : isFieldURLV2(fieldDefinition) ? (
<URLV2FieldDisplay />
) : isFieldPhone(fieldDefinition) ? (
<PhoneFieldDisplay />
) : isFieldChip(fieldDefinition) ? (

View File

@ -16,6 +16,7 @@ import { ProbabilityFieldInput } from '../meta-types/input/components/Probabilit
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 { URLV2FieldInput } from '../meta-types/input/components/URLV2FieldInput';
import { FieldInputEvent } from '../types/FieldInputEvent';
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldChip } from '../types/guards/isFieldChip';
@ -30,6 +31,7 @@ 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 { isFieldURLV2 } from '../types/guards/isFieldURLV2';
type FieldInputProps = {
onSubmit?: FieldInputEvent;
@ -98,6 +100,14 @@ export const FieldInput = ({
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldURLV2(fieldDefinition) ? (
<URLV2FieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldPhone(fieldDefinition) ? (
<PhoneFieldInput
onEnter={onEnter}

View File

@ -28,6 +28,8 @@ 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 { isFieldURLV2 } from '../types/guards/isFieldURLV2';
import { isFieldURLV2Value } from '../types/guards/isFieldURLV2Value';
import { isFieldURLValue } from '../types/guards/isFieldURLValue';
export const usePersistField = () => {
@ -66,6 +68,9 @@ export const usePersistField = () => {
const fieldIsURL =
isFieldURL(fieldDefinition) && isFieldURLValue(valueToPersist);
const fieldIsURLV2 =
isFieldURLV2(fieldDefinition) && isFieldURLV2Value(valueToPersist);
const fieldIsBoolean =
isFieldBoolean(fieldDefinition) &&
isFieldBooleanValue(valueToPersist);
@ -154,7 +159,8 @@ export const usePersistField = () => {
fieldIsNumber ||
fieldIsMoney ||
fieldIsDate ||
fieldIsPhone
fieldIsPhone ||
fieldIsURLV2
) {
const fieldName = fieldDefinition.metadata.fieldName;
@ -173,7 +179,11 @@ export const usePersistField = () => {
});
} else {
throw new Error(
`Invalid value to persist: ${valueToPersist} for type : ${fieldDefinition.type}, type may not be implemented in usePersistField.`,
`Invalid value to persist: ${JSON.stringify(
valueToPersist,
)} for type : ${
fieldDefinition.type
}, type may not be implemented in usePersistField.`,
);
}
},

View File

@ -0,0 +1,8 @@
import { useURLV2Field } from '../../hooks/useURLV2Field';
import { URLV2Display } from '../content-display/components/URLDisplayV2';
export const URLV2FieldDisplay = () => {
const { fieldValue } = useURLV2Field();
return <URLV2Display value={fieldValue} />;
};

View File

@ -0,0 +1,73 @@
import { MouseEvent } from 'react';
import styled from '@emotion/styled';
import { FieldURLV2Value } from '@/ui/data/field/types/FieldMetadata';
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
import {
LinkType,
SocialLink,
} from '@/ui/navigation/link/components/SocialLink';
import { EllipsisDisplay } from './EllipsisDisplay';
const StyledRawLink = styled(RoundedLink)`
overflow: hidden;
a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
type URLV2DisplayProps = {
value?: FieldURLV2Value;
};
const checkUrlType = (url: string) => {
if (
/^(http|https):\/\/(?:www\.)?linkedin.com(\w+:{0,1}\w*@)?(\S+)(:([0-9])+)?(\/|\/([\w#!:.?+=&%@!\-/]))?/.test(
url,
)
) {
return LinkType.LinkedIn;
}
if (url.match(/^((http|https):\/\/)?(?:www\.)?twitter\.com\/(\w+)?/i)) {
return LinkType.Twitter;
}
return LinkType.Url;
};
export const URLV2Display = ({ value }: URLV2DisplayProps) => {
const handleClick = (event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
};
const absoluteUrl = value?.link
? value.link.startsWith('http')
? value.link
: 'https://' + value.link
: '';
const displayedValue = value?.text ?? '';
const type = checkUrlType(absoluteUrl);
if (type === LinkType.LinkedIn || type === LinkType.Twitter) {
return (
<EllipsisDisplay>
<SocialLink href={absoluteUrl} onClick={handleClick} type={type}>
{displayedValue}
</SocialLink>
</EllipsisDisplay>
);
}
return (
<EllipsisDisplay>
<StyledRawLink href={absoluteUrl} onClick={handleClick}>
{displayedValue}
</StyledRawLink>
</EllipsisDisplay>
);
};

View File

@ -0,0 +1,43 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { usePersistField } from '../../hooks/usePersistField';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { FieldURLV2Value } from '../../types/FieldMetadata';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldURLV2 } from '../../types/guards/isFieldURLV2';
import { isFieldURLV2Value } from '../../types/guards/isFieldURLV2Value';
export const useURLV2Field = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('urlV2', isFieldURLV2, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldURLV2Value>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const persistField = usePersistField();
const persistURLField = (newValue: FieldURLV2Value) => {
if (!isFieldURLV2Value(newValue)) {
return;
}
persistField(newValue);
};
return {
fieldDefinition,
fieldValue,
setFieldValue,
hotkeyScope,
persistURLField,
};
};

View File

@ -0,0 +1,89 @@
import { FieldDoubleText } from '../../../types/FieldDoubleText';
import { useURLV2Field } from '../../hooks/useURLV2Field';
import { DoubleTextInput } from './internal/DoubleTextInput';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type URLV2FieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const URLV2FieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: URLV2FieldInputProps) => {
const { fieldValue, hotkeyScope, persistURLField } = useURLV2Field();
const handleEnter = (newURL: FieldDoubleText) => {
onEnter?.(() =>
persistURLField({
link: newURL.firstValue,
text: newURL.secondValue,
}),
);
};
const handleEscape = (newURL: FieldDoubleText) => {
onEscape?.(() =>
persistURLField({
link: newURL.firstValue,
text: newURL.secondValue,
}),
);
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newURL: FieldDoubleText,
) => {
onClickOutside?.(() =>
persistURLField({
link: newURL.firstValue,
text: newURL.secondValue,
}),
);
};
const handleTab = (newURL: FieldDoubleText) => {
onTab?.(() =>
persistURLField({
link: newURL.firstValue,
text: newURL.secondValue,
}),
);
};
const handleShiftTab = (newURL: FieldDoubleText) => {
onShiftTab?.(() =>
persistURLField({
link: newURL.firstValue,
text: newURL.secondValue,
}),
);
};
return (
<FieldInputOverlay>
<DoubleTextInput
firstValue={fieldValue.link}
secondValue={fieldValue.text}
firstValuePlaceholder={'Link'}
secondValuePlaceholder={'Label'}
hotkeyScope={hotkeyScope}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onTab={handleTab}
onShiftTab={handleShiftTab}
/>
</FieldInputOverlay>
);
};

View File

@ -16,6 +16,11 @@ export type FieldURLMetadata = {
fieldName: string;
};
export type FieldURLV2Metadata = {
placeHolder: string;
fieldName: string;
};
export type FieldDateMetadata = {
fieldName: string;
};
@ -82,6 +87,7 @@ export type FieldMetadata =
| FieldDoubleTextMetadata
| FieldPhoneMetadata
| FieldURLMetadata
| FieldURLV2Metadata
| FieldNumberMetadata
| FieldMoneyMetadata
| FieldEmailMetadata
@ -95,6 +101,7 @@ export type FieldChipValue = string;
export type FieldDateValue = string | null;
export type FieldPhoneValue = string;
export type FieldURLValue = string;
export type FieldURLV2Value = { link: string; text: string };
export type FieldNumberValue = number | null;
export type FieldMoneyValue = number | null;
export type FieldEmailValue = string;

View File

@ -10,5 +10,6 @@ export type FieldType =
| 'date'
| 'phone'
| 'url'
| 'urlV2'
| 'probability'
| 'moneyAmount';

View File

@ -14,6 +14,7 @@ import {
FieldRelationMetadata,
FieldTextMetadata,
FieldURLMetadata,
FieldURLV2Metadata,
} from '../FieldMetadata';
import { FieldType } from '../FieldType';
@ -41,6 +42,8 @@ type AssertFieldMetadataFunction = <
? FieldPhoneMetadata
: E extends 'url'
? FieldURLMetadata
: E extends 'urlV2'
? FieldURLV2Metadata
: E extends 'probability'
? FieldProbabilityMetadata
: E extends 'moneyAmount'

View File

@ -0,0 +1,6 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldURLV2Metadata } from '../FieldMetadata';
export const isFieldURLV2 = (
field: FieldDefinition<FieldMetadata>,
): field is FieldDefinition<FieldURLV2Metadata> => field.type === 'urlV2';

View File

@ -0,0 +1,13 @@
import { z } from 'zod';
import { FieldURLV2Value } from '../FieldMetadata';
const urlV2Schema = z.object({
link: z.string(),
text: z.string(),
});
// TODO: add yup
export const isFieldURLV2Value = (
fieldValue: unknown,
): fieldValue is FieldURLV2Value => urlV2Schema.safeParse(fieldValue).success;