mirror of
https://github.com/lingble/twenty.git
synced 2025-10-29 20:02:29 +00:00
Wrap Long text fields (textarea) (#8557)
Here we add the option for Text inputs to be wrapped, and to select on how many lines text should be displayed. Fix #7552 --------- Co-authored-by: guillim <guillaume@twenty.com>
This commit is contained in:
@@ -2,7 +2,12 @@ import { useTextFieldDisplay } from '@/object-record/record-field/meta-types/hoo
|
|||||||
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
|
import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
|
||||||
|
|
||||||
export const TextFieldDisplay = () => {
|
export const TextFieldDisplay = () => {
|
||||||
const { fieldValue } = useTextFieldDisplay();
|
const { fieldValue, fieldDefinition } = useTextFieldDisplay();
|
||||||
|
|
||||||
return <TextDisplay text={fieldValue} />;
|
return (
|
||||||
|
<TextDisplay
|
||||||
|
text={fieldValue}
|
||||||
|
displayedMaxRows={fieldDefinition.metadata?.settings?.displayedMaxRows}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import { useContext } from 'react';
|
|||||||
|
|
||||||
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
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';
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
|
|
||||||
export const useTextFieldDisplay = () => {
|
export const useTextFieldDisplay = () => {
|
||||||
const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||||
|
|
||||||
|
assertFieldMetadata(FieldMetadataType.Text, isFieldText, fieldDefinition);
|
||||||
const fieldName = fieldDefinition.metadata.fieldName;
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
|
|
||||||
const fieldValue =
|
const fieldValue =
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export type FieldTextMetadata = {
|
|||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
placeHolder: string;
|
placeHolder: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
settings?: Record<string, never>;
|
settings?: {
|
||||||
|
displayedMaxRows?: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldDateTimeMetadata = {
|
export type FieldDateTimeMetadata = {
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import { RecordInlineCellValue } from '@/object-record/record-inline-cell/compon
|
|||||||
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
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';
|
import { useRecordInlineCellContext } from './RecordInlineCellContext';
|
||||||
|
|
||||||
const StyledIconContainer = styled.div`
|
const StyledIconContainer = styled.div`
|
||||||
@@ -36,6 +39,7 @@ const StyledLabelAndIconContainer = styled.div`
|
|||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
height: 24px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledValueContainer = styled.div`
|
const StyledValueContainer = styled.div`
|
||||||
@@ -52,11 +56,12 @@ const StyledLabelContainer = styled.div<{ width?: number }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledInlineCellBaseContainer = styled.div`
|
const StyledInlineCellBaseContainer = styled.div`
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 24px;
|
height: fit-content;
|
||||||
|
line-height: 24px;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
user-select: none;
|
user-select: none;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -88,6 +93,10 @@ export const RecordInlineCellContainer = () => {
|
|||||||
|
|
||||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||||
|
|
||||||
|
if (isFieldText(fieldDefinition)) {
|
||||||
|
assertFieldMetadata(FieldMetadataType.Text, isFieldText, fieldDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
const { setIsFocused } = useFieldFocus();
|
const { setIsFocused } = useFieldFocus();
|
||||||
|
|
||||||
const handleContainerMouseEnter = () => {
|
const handleContainerMouseEnter = () => {
|
||||||
@@ -122,7 +131,7 @@ export const RecordInlineCellContainer = () => {
|
|||||||
)}
|
)}
|
||||||
{showLabel && label && (
|
{showLabel && label && (
|
||||||
<StyledLabelContainer width={labelWidth}>
|
<StyledLabelContainer width={labelWidth}>
|
||||||
<OverflowingTextWithTooltip text={label} />
|
<OverflowingTextWithTooltip text={label} isLabel={true} />
|
||||||
</StyledLabelContainer>
|
</StyledLabelContainer>
|
||||||
)}
|
)}
|
||||||
{/* TODO: Displaying Tooltips on the board is causing performance issues https://react-tooltip.com/docs/examples/render */}
|
{/* TODO: Displaying Tooltips on the board is causing performance issues https://react-tooltip.com/docs/examples/render */}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ const StyledRecordInlineCellNormalModeOuterContainer = styled.div<
|
|||||||
isDisplayModeFixHeight ? '16px' : 'auto'};
|
isDisplayModeFixHeight ? '16px' : 'auto'};
|
||||||
min-height: 16px;
|
min-height: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: ${({ theme }) => theme.spacing(1)};
|
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||||
${(props) => {
|
${(props) => {
|
||||||
if (props.isHovered === true) {
|
if (props.isHovered === true) {
|
||||||
return css`
|
return css`
|
||||||
@@ -39,15 +39,17 @@ const StyledRecordInlineCellNormalModeOuterContainer = styled.div<
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledRecordInlineCellNormalModeInnerContainer = styled.div`
|
const StyledRecordInlineCellNormalModeInnerContainer = styled.div`
|
||||||
|
align-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
font-size: 'inherit';
|
font-size: 'inherit';
|
||||||
|
|
||||||
font-weight: 'inherit';
|
font-weight: 'inherit';
|
||||||
|
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|
||||||
|
min-height: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/componen
|
|||||||
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
|
import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs';
|
||||||
import { settingsDataModelFieldBooleanFormSchema } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm';
|
import { settingsDataModelFieldBooleanFormSchema } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanForm';
|
||||||
import { SettingsDataModelFieldBooleanSettingsFormCard } from '@/settings/data-model/fields/forms/boolean/components/SettingsDataModelFieldBooleanSettingsFormCard';
|
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 { settingsDataModelFieldCurrencyFormSchema } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm';
|
||||||
import { SettingsDataModelFieldCurrencySettingsFormCard } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard';
|
import { SettingsDataModelFieldCurrencySettingsFormCard } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard';
|
||||||
import { settingsDataModelFieldDateFormSchema } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
|
import { settingsDataModelFieldDateFormSchema } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
|
||||||
@@ -58,6 +60,10 @@ const numberFieldFormSchema = z
|
|||||||
.object({ type: z.literal(FieldMetadataType.Number) })
|
.object({ type: z.literal(FieldMetadataType.Number) })
|
||||||
.merge(settingsDataModelFieldNumberFormSchema);
|
.merge(settingsDataModelFieldNumberFormSchema);
|
||||||
|
|
||||||
|
const textFieldFormSchema = z
|
||||||
|
.object({ type: z.literal(FieldMetadataType.Text) })
|
||||||
|
.merge(settingsDataModelFieldtextFormSchema);
|
||||||
|
|
||||||
const otherFieldsFormSchema = z.object({
|
const otherFieldsFormSchema = z.object({
|
||||||
type: z.enum(
|
type: z.enum(
|
||||||
Object.keys(
|
Object.keys(
|
||||||
@@ -70,6 +76,7 @@ const otherFieldsFormSchema = z.object({
|
|||||||
FieldMetadataType.Date,
|
FieldMetadataType.Date,
|
||||||
FieldMetadataType.DateTime,
|
FieldMetadataType.DateTime,
|
||||||
FieldMetadataType.Number,
|
FieldMetadataType.Number,
|
||||||
|
FieldMetadataType.Text,
|
||||||
]),
|
]),
|
||||||
) as [FieldMetadataType, ...FieldMetadataType[]],
|
) as [FieldMetadataType, ...FieldMetadataType[]],
|
||||||
),
|
),
|
||||||
@@ -86,6 +93,7 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
|
|||||||
selectFieldFormSchema,
|
selectFieldFormSchema,
|
||||||
multiSelectFieldFormSchema,
|
multiSelectFieldFormSchema,
|
||||||
numberFieldFormSchema,
|
numberFieldFormSchema,
|
||||||
|
textFieldFormSchema,
|
||||||
otherFieldsFormSchema,
|
otherFieldsFormSchema,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -183,6 +191,15 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fieldMetadataItem.type === FieldMetadataType.Text) {
|
||||||
|
return (
|
||||||
|
<SettingsDataModelFieldTextSettingsFormCard
|
||||||
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
fieldMetadataItem.type === FieldMetadataType.Select ||
|
fieldMetadataItem.type === FieldMetadataType.Select ||
|
||||||
fieldMetadataItem.type === FieldMetadataType.MultiSelect
|
fieldMetadataItem.type === FieldMetadataType.MultiSelect
|
||||||
|
|||||||
@@ -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<SettingsDataModelFieldTextFormValues>();
|
||||||
|
return (
|
||||||
|
<CardContent>
|
||||||
|
<Controller
|
||||||
|
name="settings"
|
||||||
|
defaultValue={{
|
||||||
|
displayedMaxRows: fieldMetadataItem?.settings?.displayedMaxRows || 0,
|
||||||
|
}}
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => {
|
||||||
|
const displayedMaxRows = value?.displayedMaxRows ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledFormCardTitle>Wrap on record pages</StyledFormCardTitle>
|
||||||
|
<Select
|
||||||
|
disabled={disabled}
|
||||||
|
dropdownId="selectTextWrap"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: 'Deactivated',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'First 2 lines',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'First 5 lines',
|
||||||
|
value: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'First 10 lines',
|
||||||
|
value: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'All lines',
|
||||||
|
value: 99,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={displayedMaxRows}
|
||||||
|
onChange={(value) => onChange({ displayedMaxRows: value })}
|
||||||
|
withSearchInput={false}
|
||||||
|
dropdownWidthAuto={true}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>;
|
||||||
|
|
||||||
|
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||||
|
flex: 1 1 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsDataModelFieldTextSettingsFormCard = ({
|
||||||
|
disabled,
|
||||||
|
fieldMetadataItem,
|
||||||
|
objectMetadataItem,
|
||||||
|
}: SettingsDataModelFieldTextSettingsFormCardProps) => {
|
||||||
|
return (
|
||||||
|
<SettingsDataModelPreviewFormCard
|
||||||
|
preview={
|
||||||
|
<StyledFieldPreviewCard
|
||||||
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
form={
|
||||||
|
<SettingsDataModelFieldTextForm
|
||||||
|
disabled={disabled}
|
||||||
|
fieldMetadataItem={fieldMetadataItem}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -30,18 +30,21 @@ export type SettingsDataModelFieldPreviewProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const StyledFieldPreview = styled.div<{ shrink?: boolean }>`
|
const StyledFieldPreview = styled.div<{ shrink?: boolean }>`
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
background-color: ${({ theme }) => theme.background.primary};
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
height: ${({ theme }) => theme.spacing(8)};
|
height: fit-content;
|
||||||
|
line-height: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0
|
padding: 0
|
||||||
${({ shrink, theme }) => (shrink ? theme.spacing(1) : theme.spacing(2))};
|
${({ shrink, theme }) => (shrink ? theme.spacing(1) : theme.spacing(2))};
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledFieldLabel = styled.div`
|
const StyledFieldLabel = styled.div`
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { TextDisplay } from './TextDisplay';
|
|
||||||
|
|
||||||
export const DoubleTextDisplay = TextDisplay;
|
|
||||||
@@ -2,8 +2,9 @@ import { OverflowingTextWithTooltip } from 'twenty-ui';
|
|||||||
|
|
||||||
type TextDisplayProps = {
|
type TextDisplayProps = {
|
||||||
text: string;
|
text: string;
|
||||||
|
displayedMaxRows?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextDisplay = ({ text }: TextDisplayProps) => (
|
export const TextDisplay = ({ text, displayedMaxRows }: TextDisplayProps) => (
|
||||||
<OverflowingTextWithTooltip text={text} />
|
<OverflowingTextWithTooltip text={text} displayedMaxRows={displayedMaxRows} />
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
IsEnum,
|
IsEnum,
|
||||||
IsInt,
|
IsInt,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
|
Max,
|
||||||
Min,
|
Min,
|
||||||
validateOrReject,
|
validateOrReject,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
@@ -31,6 +32,12 @@ class SettingsValidation {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsEnum(ValueType)
|
@IsEnum(ValueType)
|
||||||
type?: 'percentage' | 'number';
|
type?: 'percentage' | 'number';
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Max(100)
|
||||||
|
displayedMaxRows?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -48,23 +55,26 @@ export class FieldMetadataValidationService<
|
|||||||
}) {
|
}) {
|
||||||
switch (fieldType) {
|
switch (fieldType) {
|
||||||
case FieldMetadataType.NUMBER:
|
case FieldMetadataType.NUMBER:
|
||||||
await this.validateNumberSettings(settings);
|
case FieldMetadataType.TEXT:
|
||||||
|
await this.validateSettings(settings);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateNumberSettings(settings: any) {
|
private async validateSettings(settings: any) {
|
||||||
try {
|
try {
|
||||||
const settingsInstance = plainToInstance(SettingsValidation, settings);
|
const settingsInstance = plainToInstance(SettingsValidation, settings);
|
||||||
|
|
||||||
await validateOrReject(settingsInstance);
|
await validateOrReject(settingsInstance);
|
||||||
} catch (errors) {
|
} catch (error) {
|
||||||
const errorMessages = errors
|
const errorMessages = Array.isArray(error)
|
||||||
.map((error: any) => Object.values(error.constraints))
|
? error
|
||||||
|
.map((err: any) => Object.values(err.constraints))
|
||||||
.flat()
|
.flat()
|
||||||
.join(', ');
|
.join(', ')
|
||||||
|
: error.message;
|
||||||
|
|
||||||
throw new FieldMetadataException(
|
throw new FieldMetadataException(
|
||||||
`Value for settings is invalid: ${errorMessages}`,
|
`Value for settings is invalid: ${errorMessages}`,
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ type FieldMetadataNumberSettings = {
|
|||||||
type?: string;
|
type?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FieldMetadataTextSettings = {
|
||||||
|
displayedMaxRows?: number;
|
||||||
|
};
|
||||||
|
|
||||||
type FieldMetadataDateSettings = {
|
type FieldMetadataDateSettings = {
|
||||||
displayAsRelativeDate?: boolean;
|
displayAsRelativeDate?: boolean;
|
||||||
};
|
};
|
||||||
@@ -28,6 +32,7 @@ type FieldMetadataSettingsMapping = {
|
|||||||
[FieldMetadataType.NUMBER]: FieldMetadataNumberSettings;
|
[FieldMetadataType.NUMBER]: FieldMetadataNumberSettings;
|
||||||
[FieldMetadataType.DATE]: FieldMetadataDateSettings;
|
[FieldMetadataType.DATE]: FieldMetadataDateSettings;
|
||||||
[FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings;
|
[FieldMetadataType.DATE_TIME]: FieldMetadataDateTimeSettings;
|
||||||
|
[FieldMetadataType.TEXT]: FieldMetadataTextSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SettingsByFieldMetadata<T extends FieldMetadataType | 'default'> =
|
type SettingsByFieldMetadata<T extends FieldMetadataType | 'default'> =
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const spacing4 = THEME_COMMON.spacing(4);
|
|||||||
const StyledOverflowingText = styled.div<{
|
const StyledOverflowingText = styled.div<{
|
||||||
cursorPointer: boolean;
|
cursorPointer: boolean;
|
||||||
size: 'large' | 'small';
|
size: 'large' | 'small';
|
||||||
|
displayedMaxRows?: number;
|
||||||
|
isLabel: boolean;
|
||||||
}>`
|
}>`
|
||||||
cursor: ${({ cursorPointer }) => (cursorPointer ? 'pointer' : 'inherit')};
|
cursor: ${({ cursorPointer }) => (cursorPointer ? 'pointer' : 'inherit')};
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
@@ -26,6 +28,15 @@ const StyledOverflowingText = styled.div<{
|
|||||||
|
|
||||||
height: ${({ size }) => (size === 'large' ? spacing4 : 'auto')};
|
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 {
|
& :hover {
|
||||||
text-overflow: ${({ cursorPointer }) =>
|
text-overflow: ${({ cursorPointer }) =>
|
||||||
cursorPointer ? 'clip' : 'ellipsis'};
|
cursorPointer ? 'clip' : 'ellipsis'};
|
||||||
@@ -37,11 +48,15 @@ const StyledOverflowingText = styled.div<{
|
|||||||
export const OverflowingTextWithTooltip = ({
|
export const OverflowingTextWithTooltip = ({
|
||||||
size = 'small',
|
size = 'small',
|
||||||
text,
|
text,
|
||||||
mutliline,
|
isTooltipMultiline,
|
||||||
|
displayedMaxRows,
|
||||||
|
isLabel,
|
||||||
}: {
|
}: {
|
||||||
size?: 'large' | 'small';
|
size?: 'large' | 'small';
|
||||||
text: string | null | undefined;
|
text: string | null | undefined;
|
||||||
mutliline?: boolean;
|
isTooltipMultiline?: boolean;
|
||||||
|
displayedMaxRows?: number;
|
||||||
|
isLabel?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const textElementId = `title-id-${+new Date()}`;
|
const textElementId = `title-id-${+new Date()}`;
|
||||||
|
|
||||||
@@ -74,6 +89,8 @@ export const OverflowingTextWithTooltip = ({
|
|||||||
data-testid="tooltip"
|
data-testid="tooltip"
|
||||||
cursorPointer={isTitleOverflowing}
|
cursorPointer={isTitleOverflowing}
|
||||||
size={size}
|
size={size}
|
||||||
|
displayedMaxRows={displayedMaxRows}
|
||||||
|
isLabel={isLabel ?? false}
|
||||||
ref={textRef}
|
ref={textRef}
|
||||||
id={textElementId}
|
id={textElementId}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
@@ -86,7 +103,7 @@ export const OverflowingTextWithTooltip = ({
|
|||||||
<div onClick={handleTooltipClick}>
|
<div onClick={handleTooltipClick}>
|
||||||
<AppTooltip
|
<AppTooltip
|
||||||
anchorSelect={`#${textElementId}`}
|
anchorSelect={`#${textElementId}`}
|
||||||
content={mutliline ? undefined : (text ?? '')}
|
content={isTooltipMultiline ? undefined : (text ?? '')}
|
||||||
offset={5}
|
offset={5}
|
||||||
isOpen
|
isOpen
|
||||||
noArrow
|
noArrow
|
||||||
@@ -94,7 +111,7 @@ export const OverflowingTextWithTooltip = ({
|
|||||||
positionStrategy="absolute"
|
positionStrategy="absolute"
|
||||||
delay={TooltipDelay.mediumDelay}
|
delay={TooltipDelay.mediumDelay}
|
||||||
>
|
>
|
||||||
{mutliline ? <pre>{text}</pre> : ''}
|
{isTooltipMultiline ? <pre>{text}</pre> : ''}
|
||||||
</AppTooltip>
|
</AppTooltip>
|
||||||
</div>,
|
</div>,
|
||||||
document.body,
|
document.body,
|
||||||
|
|||||||
Reference in New Issue
Block a user