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:
@ -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) ? (
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@ -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} />;
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -10,5 +10,6 @@ export type FieldType =
|
||||
| 'date'
|
||||
| 'phone'
|
||||
| 'url'
|
||||
| 'urlV2'
|
||||
| 'probability'
|
||||
| 'moneyAmount';
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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';
|
||||
@ -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;
|
||||
Reference in New Issue
Block a user