diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/TextFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/TextFieldDisplay.tsx
index ce56ff7a6..803afa626 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/TextFieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/TextFieldDisplay.tsx
@@ -2,7 +2,12 @@ import { useTextFieldDisplay } from '@/object-record/record-field/meta-types/hoo
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
export const TextFieldDisplay = () => {
- const { fieldValue } = useTextFieldDisplay();
+ const { fieldValue, fieldDefinition } = useTextFieldDisplay();
- return ;
+ return (
+
+ );
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useTextFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useTextFieldDisplay.ts
index 0eb831aea..0bd7c660f 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useTextFieldDisplay.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useTextFieldDisplay.ts
@@ -2,11 +2,15 @@ import { useContext } from 'react';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
+import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
+import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
+import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '../../contexts/FieldContext';
export const useTextFieldDisplay = () => {
const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
+ assertFieldMetadata(FieldMetadataType.Text, isFieldText, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue =
diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts
index 53e037dbd..f739af431 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts
@@ -23,7 +23,9 @@ export type FieldTextMetadata = {
objectMetadataNameSingular?: string;
placeHolder: string;
fieldName: string;
- settings?: Record;
+ settings?: {
+ displayedMaxRows?: number;
+ };
};
export type FieldDateTimeMetadata = {
diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx
index 740039656..ed0b7c83a 100644
--- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx
@@ -14,6 +14,9 @@ import { RecordInlineCellValue } from '@/object-record/record-inline-cell/compon
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
+import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
+import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
+import { FieldMetadataType } from '~/generated-metadata/graphql';
import { useRecordInlineCellContext } from './RecordInlineCellContext';
const StyledIconContainer = styled.div`
@@ -36,6 +39,7 @@ const StyledLabelAndIconContainer = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
+ height: 24px;
`;
const StyledValueContainer = styled.div`
@@ -52,11 +56,12 @@ const StyledLabelContainer = styled.div<{ width?: number }>`
`;
const StyledInlineCellBaseContainer = styled.div`
- align-items: center;
+ align-items: flex-start;
box-sizing: border-box;
width: 100%;
display: flex;
- height: 24px;
+ height: fit-content;
+ line-height: 24px;
gap: ${({ theme }) => theme.spacing(1)};
user-select: none;
justify-content: center;
@@ -88,6 +93,10 @@ export const RecordInlineCellContainer = () => {
const { recordId, fieldDefinition } = useContext(FieldContext);
+ if (isFieldText(fieldDefinition)) {
+ assertFieldMetadata(FieldMetadataType.Text, isFieldText, fieldDefinition);
+ }
+
const { setIsFocused } = useFieldFocus();
const handleContainerMouseEnter = () => {
@@ -122,7 +131,7 @@ export const RecordInlineCellContainer = () => {
)}
{showLabel && label && (
-
+
)}
{/* TODO: Displaying Tooltips on the board is causing performance issues https://react-tooltip.com/docs/examples/render */}
diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellDisplayMode.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellDisplayMode.tsx
index cf57e47ac..857df1b0d 100644
--- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellDisplayMode.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellDisplayMode.tsx
@@ -23,8 +23,8 @@ const StyledRecordInlineCellNormalModeOuterContainer = styled.div<
isDisplayModeFixHeight ? '16px' : 'auto'};
min-height: 16px;
overflow: hidden;
- padding: ${({ theme }) => theme.spacing(1)};
-
+ padding-right: ${({ theme }) => theme.spacing(1)};
+ padding-left: ${({ theme }) => theme.spacing(1)};
${(props) => {
if (props.isHovered === true) {
return css`
@@ -39,15 +39,17 @@ const StyledRecordInlineCellNormalModeOuterContainer = styled.div<
`;
const StyledRecordInlineCellNormalModeInnerContainer = styled.div`
+ align-content: center;
align-items: center;
color: ${({ theme }) => theme.font.color.primary};
font-size: 'inherit';
+
font-weight: 'inherit';
height: fit-content;
+ min-height: 24px;
overflow: hidden;
-
text-overflow: ellipsis;
white-space: nowrap;
`;
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
index 4614b607b..281c20522 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
@@ -7,6 +7,8 @@ import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/componen
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
import { settingsDataModelFieldBooleanFormSchema } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm';
import { SettingsDataModelFieldBooleanSettingsFormCard } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanSettingsFormCard';
+import { settingsDataModelFieldtextFormSchema } from '@/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm';
+import { SettingsDataModelFieldTextSettingsFormCard } from '@/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextSettingsFormCard';
import { settingsDataModelFieldCurrencyFormSchema } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm';
import { SettingsDataModelFieldCurrencySettingsFormCard } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard';
import { settingsDataModelFieldDateFormSchema } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
@@ -58,6 +60,10 @@ const numberFieldFormSchema = z
.object({ type: z.literal(FieldMetadataType.Number) })
.merge(settingsDataModelFieldNumberFormSchema);
+const textFieldFormSchema = z
+ .object({ type: z.literal(FieldMetadataType.Text) })
+ .merge(settingsDataModelFieldtextFormSchema);
+
const otherFieldsFormSchema = z.object({
type: z.enum(
Object.keys(
@@ -70,6 +76,7 @@ const otherFieldsFormSchema = z.object({
FieldMetadataType.Date,
FieldMetadataType.DateTime,
FieldMetadataType.Number,
+ FieldMetadataType.Text,
]),
) as [FieldMetadataType, ...FieldMetadataType[]],
),
@@ -86,6 +93,7 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
selectFieldFormSchema,
multiSelectFieldFormSchema,
numberFieldFormSchema,
+ textFieldFormSchema,
otherFieldsFormSchema,
],
);
@@ -183,6 +191,15 @@ export const SettingsDataModelFieldSettingsFormCard = ({
);
}
+ if (fieldMetadataItem.type === FieldMetadataType.Text) {
+ return (
+
+ );
+ }
+
if (
fieldMetadataItem.type === FieldMetadataType.Select ||
fieldMetadataItem.type === FieldMetadataType.MultiSelect
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm.tsx
new file mode 100644
index 000000000..2c10f1f1f
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm.tsx
@@ -0,0 +1,91 @@
+import { Controller, useFormContext } from 'react-hook-form';
+
+import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { Select } from '@/ui/input/components/Select';
+import styled from '@emotion/styled';
+import { CardContent } from 'twenty-ui';
+import { z } from 'zod';
+
+const StyledFormCardTitle = styled.div`
+ color: ${({ theme }) => theme.font.color.light};
+ font-size: ${({ theme }) => theme.font.size.xs};
+ font-weight: ${({ theme }) => theme.font.weight.semiBold};
+ margin-bottom: ${({ theme }) => theme.spacing(1)};
+`;
+
+type SettingsDataModelFieldTextFormProps = {
+ disabled?: boolean;
+ fieldMetadataItem: Pick<
+ FieldMetadataItem,
+ 'icon' | 'label' | 'type' | 'defaultValue' | 'settings'
+ >;
+};
+
+export const textFieldDefaultValueSchema = z.object({
+ displayedMaxRows: z.number().nullable(),
+});
+
+export const settingsDataModelFieldtextFormSchema = z.object({
+ settings: textFieldDefaultValueSchema,
+});
+
+export type SettingsDataModelFieldTextFormValues = z.infer<
+ typeof settingsDataModelFieldtextFormSchema
+>;
+
+export const SettingsDataModelFieldTextForm = ({
+ disabled,
+ fieldMetadataItem,
+}: SettingsDataModelFieldTextFormProps) => {
+ const { control } = useFormContext();
+ return (
+
+ {
+ const displayedMaxRows = value?.displayedMaxRows ?? 0;
+
+ return (
+ <>
+ Wrap on record pages
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextSettingsFormCard.tsx
new file mode 100644
index 000000000..7dea46004
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextSettingsFormCard.tsx
@@ -0,0 +1,45 @@
+import styled from '@emotion/styled';
+
+import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
+
+import { SettingsDataModelFieldTextForm } from '@/settings/data-model/fields/forms/components/text/SettingsDataModelFieldTextForm';
+import {
+ SettingsDataModelFieldPreviewCard,
+ SettingsDataModelFieldPreviewCardProps,
+} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
+
+type SettingsDataModelFieldTextSettingsFormCardProps = {
+ disabled?: boolean;
+ fieldMetadataItem: Pick<
+ FieldMetadataItem,
+ 'icon' | 'label' | 'type' | 'defaultValue'
+ >;
+} & Pick;
+
+const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
+ flex: 1 1 100%;
+`;
+
+export const SettingsDataModelFieldTextSettingsFormCard = ({
+ disabled,
+ fieldMetadataItem,
+ objectMetadataItem,
+}: SettingsDataModelFieldTextSettingsFormCardProps) => {
+ return (
+
+ }
+ form={
+
+ }
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx
index 1fbefb2d3..c0ec0b188 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx
@@ -30,18 +30,21 @@ export type SettingsDataModelFieldPreviewProps = {
};
const StyledFieldPreview = styled.div<{ shrink?: boolean }>`
- align-items: center;
+ align-items: flex-start;
background-color: ${({ theme }) => theme.background.primary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
- height: ${({ theme }) => theme.spacing(8)};
+ height: fit-content;
+ line-height: 24px;
overflow: hidden;
padding: 0
${({ shrink, theme }) => (shrink ? theme.spacing(1) : theme.spacing(2))};
white-space: nowrap;
margin-top: ${({ theme }) => theme.spacing(2)};
+ padding-top: ${({ theme }) => theme.spacing(2)};
+ padding-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledFieldLabel = styled.div`
diff --git a/packages/twenty-front/src/modules/ui/field/display/components/DoubleTextDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/DoubleTextDisplay.tsx
deleted file mode 100644
index d8c2ac0b1..000000000
--- a/packages/twenty-front/src/modules/ui/field/display/components/DoubleTextDisplay.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import { TextDisplay } from './TextDisplay';
-
-export const DoubleTextDisplay = TextDisplay;
diff --git a/packages/twenty-front/src/modules/ui/field/display/components/TextDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/TextDisplay.tsx
index 74f7f002d..0b9211e2f 100644
--- a/packages/twenty-front/src/modules/ui/field/display/components/TextDisplay.tsx
+++ b/packages/twenty-front/src/modules/ui/field/display/components/TextDisplay.tsx
@@ -2,8 +2,9 @@ import { OverflowingTextWithTooltip } from 'twenty-ui';
type TextDisplayProps = {
text: string;
+ displayedMaxRows?: number;
};
-export const TextDisplay = ({ text }: TextDisplayProps) => (
-
+export const TextDisplay = ({ text, displayedMaxRows }: TextDisplayProps) => (
+
);
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata-validation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata-validation.service.ts
index 35b735fb2..5eec57d2a 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata-validation.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata-validation.service.ts
@@ -5,6 +5,7 @@ import {
IsEnum,
IsInt,
IsOptional,
+ Max,
Min,
validateOrReject,
} from 'class-validator';
@@ -31,6 +32,12 @@ class SettingsValidation {
@IsOptional()
@IsEnum(ValueType)
type?: 'percentage' | 'number';
+
+ @IsOptional()
+ @IsInt()
+ @Min(0)
+ @Max(100)
+ displayedMaxRows?: number;
}
@Injectable()
@@ -48,23 +55,26 @@ export class FieldMetadataValidationService<
}) {
switch (fieldType) {
case FieldMetadataType.NUMBER:
- await this.validateNumberSettings(settings);
+ case FieldMetadataType.TEXT:
+ await this.validateSettings(settings);
break;
default:
break;
}
}
- private async validateNumberSettings(settings: any) {
+ private async validateSettings(settings: any) {
try {
const settingsInstance = plainToInstance(SettingsValidation, settings);
await validateOrReject(settingsInstance);
- } catch (errors) {
- const errorMessages = errors
- .map((error: any) => Object.values(error.constraints))
- .flat()
- .join(', ');
+ } catch (error) {
+ const errorMessages = Array.isArray(error)
+ ? error
+ .map((err: any) => Object.values(err.constraints))
+ .flat()
+ .join(', ')
+ : error.message;
throw new FieldMetadataException(
`Value for settings is invalid: ${errorMessages}`,
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts
index 727cbe2cf..6afede943 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts
@@ -16,6 +16,10 @@ type FieldMetadataNumberSettings = {
type?: string;
};
+type FieldMetadataTextSettings = {
+ displayedMaxRows?: number;
+};
+
type FieldMetadataDateSettings = {
displayAsRelativeDate?: boolean;
};
@@ -28,6 +32,7 @@ type FieldMetadataSettingsMapping = {
[FieldMetadataType.NUMBER]: FieldMetadataNumberSettings;
[FieldMetadataType.DATE]: FieldMetadataDateSettings;
[FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings;
+ [FieldMetadataType.TEXT]: FieldMetadataTextSettings;
};
type SettingsByFieldMetadata =
diff --git a/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx
index 7f8df1450..76dcee3d9 100644
--- a/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx
+++ b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx
@@ -11,6 +11,8 @@ const spacing4 = THEME_COMMON.spacing(4);
const StyledOverflowingText = styled.div<{
cursorPointer: boolean;
size: 'large' | 'small';
+ displayedMaxRows?: number;
+ isLabel: boolean;
}>`
cursor: ${({ cursorPointer }) => (cursorPointer ? 'pointer' : 'inherit')};
font-family: inherit;
@@ -26,6 +28,15 @@ const StyledOverflowingText = styled.div<{
height: ${({ size }) => (size === 'large' ? spacing4 : 'auto')};
+ text-wrap-mode: ${({ isLabel, displayedMaxRows }) =>
+ isLabel === false && displayedMaxRows ? 'wrap' : 'nowrap'};
+ -webkit-line-clamp: ${({ isLabel, displayedMaxRows }) =>
+ isLabel === false && displayedMaxRows ? displayedMaxRows : 'inherit'};
+ display: ${({ isLabel, displayedMaxRows }) =>
+ isLabel === false && displayedMaxRows ? `-webkit-box` : 'block'};
+ -webkit-box-orient: ${({ isLabel, displayedMaxRows }) =>
+ isLabel === false && displayedMaxRows ? 'vertical' : 'inherit'};
+
& :hover {
text-overflow: ${({ cursorPointer }) =>
cursorPointer ? 'clip' : 'ellipsis'};
@@ -37,11 +48,15 @@ const StyledOverflowingText = styled.div<{
export const OverflowingTextWithTooltip = ({
size = 'small',
text,
- mutliline,
+ isTooltipMultiline,
+ displayedMaxRows,
+ isLabel,
}: {
size?: 'large' | 'small';
text: string | null | undefined;
- mutliline?: boolean;
+ isTooltipMultiline?: boolean;
+ displayedMaxRows?: number;
+ isLabel?: boolean;
}) => {
const textElementId = `title-id-${+new Date()}`;
@@ -74,6 +89,8 @@ export const OverflowingTextWithTooltip = ({
data-testid="tooltip"
cursorPointer={isTitleOverflowing}
size={size}
+ displayedMaxRows={displayedMaxRows}
+ isLabel={isLabel ?? false}
ref={textRef}
id={textElementId}
onMouseEnter={handleMouseEnter}
@@ -86,7 +103,7 @@ export const OverflowingTextWithTooltip = ({
- {mutliline ? {text} : ''}
+ {isTooltipMultiline ? {text} : ''}
,
document.body,