Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7f9e829b6 | ||
|
|
634977e00e |
+1
@@ -14,4 +14,5 @@ export const SORTABLE_FIELD_METADATA_TYPES = [
|
||||
FieldMetadataType.ACTOR,
|
||||
FieldMetadataType.LINKS,
|
||||
FieldMetadataType.PHONES,
|
||||
FieldMetadataType.ADDRESS,
|
||||
];
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
|
||||
import {
|
||||
type FieldMetadataMultiItemSettings,
|
||||
type FieldMetadataSettingsMapping,
|
||||
type FieldMetadataType,
|
||||
type PartialFieldMetadataItemOption,
|
||||
} from 'twenty-shared/types';
|
||||
import { type ThemeColor } from 'twenty-ui/theme';
|
||||
@@ -36,6 +38,8 @@ export type FieldMetadataItem = Omit<
|
||||
| FieldDateMetadataSettings
|
||||
| FieldMetadataMultiItemSettings
|
||||
| FieldRelationMetadataSettings
|
||||
| FieldMetadataSettingsMapping[FieldMetadataType.ADDRESS]
|
||||
| FieldMetadataSettingsMapping[FieldMetadataType.FULL_NAME]
|
||||
| null;
|
||||
isLabelSyncedWithName?: boolean | null;
|
||||
morphId?: string | null;
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
ADDRESS_DEFAULT_SORT_SUB_FIELD,
|
||||
getDefaultSortSubFieldForAddress,
|
||||
} from '@/object-metadata/utils/getDefaultSortSubFieldForAddress';
|
||||
|
||||
describe('getDefaultSortSubFieldForAddress', () => {
|
||||
it('returns the configured sub-field when it is in the enabled subFields', () => {
|
||||
expect(
|
||||
getDefaultSortSubFieldForAddress({
|
||||
subFields: ['addressStreet1', 'addressState'],
|
||||
defaultSortSubField: 'addressState',
|
||||
}),
|
||||
).toBe('addressState');
|
||||
});
|
||||
|
||||
it('returns the configured sub-field when subFields is unset', () => {
|
||||
expect(
|
||||
getDefaultSortSubFieldForAddress({
|
||||
defaultSortSubField: 'addressCountry',
|
||||
}),
|
||||
).toBe('addressCountry');
|
||||
});
|
||||
|
||||
it('falls back to addressCity when no sub-field is configured', () => {
|
||||
expect(getDefaultSortSubFieldForAddress(null)).toBe(
|
||||
ADDRESS_DEFAULT_SORT_SUB_FIELD,
|
||||
);
|
||||
expect(getDefaultSortSubFieldForAddress(undefined)).toBe(
|
||||
ADDRESS_DEFAULT_SORT_SUB_FIELD,
|
||||
);
|
||||
expect(getDefaultSortSubFieldForAddress({})).toBe(
|
||||
ADDRESS_DEFAULT_SORT_SUB_FIELD,
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to addressCity when the configured sub-field is disabled', () => {
|
||||
expect(
|
||||
getDefaultSortSubFieldForAddress({
|
||||
subFields: ['addressStreet1', 'addressCity'],
|
||||
defaultSortSubField: 'addressState',
|
||||
}),
|
||||
).toBe('addressCity');
|
||||
});
|
||||
|
||||
it('falls back to the first enabled sub-field when addressCity is disabled', () => {
|
||||
expect(
|
||||
getDefaultSortSubFieldForAddress({
|
||||
subFields: ['addressStreet1', 'addressState'],
|
||||
}),
|
||||
).toBe('addressStreet1');
|
||||
});
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
FULL_NAME_DEFAULT_SORT_SUB_FIELD,
|
||||
getDefaultSortSubFieldForFullName,
|
||||
} from '@/object-metadata/utils/getDefaultSortSubFieldForFullName';
|
||||
|
||||
describe('getDefaultSortSubFieldForFullName', () => {
|
||||
it('returns the configured sub-field when set', () => {
|
||||
expect(
|
||||
getDefaultSortSubFieldForFullName({ defaultSortSubField: 'lastName' }),
|
||||
).toBe('lastName');
|
||||
});
|
||||
|
||||
it('falls back to firstName when settings are null', () => {
|
||||
expect(getDefaultSortSubFieldForFullName(null)).toBe(
|
||||
FULL_NAME_DEFAULT_SORT_SUB_FIELD,
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to firstName when settings are undefined', () => {
|
||||
expect(getDefaultSortSubFieldForFullName(undefined)).toBe(
|
||||
FULL_NAME_DEFAULT_SORT_SUB_FIELD,
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to firstName when defaultSortSubField is not set', () => {
|
||||
expect(getDefaultSortSubFieldForFullName({})).toBe(
|
||||
FULL_NAME_DEFAULT_SORT_SUB_FIELD,
|
||||
);
|
||||
});
|
||||
});
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getOrderByForFieldMetadataType } from '@/object-metadata/utils/getOrderByForFieldMetadataType';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
const buildField = (
|
||||
overrides: Pick<FieldMetadataItem, 'type' | 'name'> &
|
||||
Partial<Pick<FieldMetadataItem, 'id' | 'settings'>>,
|
||||
): Pick<FieldMetadataItem, 'id' | 'name' | 'type' | 'settings'> => ({
|
||||
id: 'field-id',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('getOrderByForFieldMetadataType', () => {
|
||||
describe('FULL_NAME', () => {
|
||||
it('sorts by firstName then lastName when no setting is configured', () => {
|
||||
const field = buildField({
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
name: 'name',
|
||||
});
|
||||
|
||||
expect(getOrderByForFieldMetadataType(field, 'AscNullsLast')).toEqual([
|
||||
{
|
||||
name: {
|
||||
firstName: 'AscNullsLast',
|
||||
lastName: 'AscNullsLast',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the configured sub-field as the primary sort key', () => {
|
||||
const field = buildField({
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
name: 'name',
|
||||
settings: { defaultSortSubField: 'lastName' },
|
||||
});
|
||||
|
||||
const result = getOrderByForFieldMetadataType(field, 'DescNullsLast');
|
||||
const keys = Object.keys(result[0].name as Record<string, unknown>);
|
||||
|
||||
expect(keys[0]).toBe('lastName');
|
||||
expect(keys[1]).toBe('firstName');
|
||||
expect((result[0].name as Record<string, string>).lastName).toBe(
|
||||
'DescNullsLast',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ADDRESS', () => {
|
||||
it('falls back to addressCity when no sub-field is configured', () => {
|
||||
const field = buildField({
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
name: 'address',
|
||||
});
|
||||
|
||||
expect(getOrderByForFieldMetadataType(field, 'AscNullsLast')).toEqual([
|
||||
{
|
||||
address: {
|
||||
addressCity: 'AscNullsLast',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses the configured sub-field', () => {
|
||||
const field = buildField({
|
||||
type: FieldMetadataType.ADDRESS,
|
||||
name: 'address',
|
||||
settings: {
|
||||
subFields: ['addressStreet1', 'addressCountry'],
|
||||
defaultSortSubField: 'addressCountry',
|
||||
},
|
||||
});
|
||||
|
||||
expect(getOrderByForFieldMetadataType(field, 'DescNullsLast')).toEqual([
|
||||
{
|
||||
address: {
|
||||
addressCountry: 'DescNullsLast',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getEnabledAddressSubFields } from '@/object-metadata/utils/getEnabledAddressSubFields';
|
||||
import {
|
||||
type AllowedAddressSubField,
|
||||
type FieldMetadataSettingsMapping,
|
||||
type FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const ADDRESS_DEFAULT_SORT_SUB_FIELD: AllowedAddressSubField =
|
||||
'addressCity';
|
||||
|
||||
type AddressSettings = FieldMetadataSettingsMapping[FieldMetadataType.ADDRESS];
|
||||
|
||||
export const getDefaultSortSubFieldForAddress = (
|
||||
settings: FieldMetadataItem['settings'] | null | undefined,
|
||||
): AllowedAddressSubField => {
|
||||
const addressSettings = settings as AddressSettings | null | undefined;
|
||||
const enabledSubFields = getEnabledAddressSubFields(settings);
|
||||
|
||||
const configuredSubField = addressSettings?.defaultSortSubField;
|
||||
if (configuredSubField && enabledSubFields.includes(configuredSubField)) {
|
||||
return configuredSubField;
|
||||
}
|
||||
|
||||
if (enabledSubFields.includes(ADDRESS_DEFAULT_SORT_SUB_FIELD)) {
|
||||
return ADDRESS_DEFAULT_SORT_SUB_FIELD;
|
||||
}
|
||||
|
||||
return enabledSubFields[0] ?? ADDRESS_DEFAULT_SORT_SUB_FIELD;
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import {
|
||||
type AllowedFullNameSubField,
|
||||
type FieldMetadataSettingsMapping,
|
||||
type FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const FULL_NAME_DEFAULT_SORT_SUB_FIELD: AllowedFullNameSubField =
|
||||
'firstName';
|
||||
|
||||
type FullNameSettings =
|
||||
FieldMetadataSettingsMapping[FieldMetadataType.FULL_NAME];
|
||||
|
||||
export const getDefaultSortSubFieldForFullName = (
|
||||
settings: FieldMetadataItem['settings'] | null | undefined,
|
||||
): AllowedFullNameSubField => {
|
||||
const fullNameSettings = settings as FullNameSettings | null | undefined;
|
||||
return (
|
||||
fullNameSettings?.defaultSortSubField ?? FULL_NAME_DEFAULT_SORT_SUB_FIELD
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import {
|
||||
DEFAULT_VISIBLE_ADDRESS_SUBFIELDS,
|
||||
type AllowedAddressSubField,
|
||||
type FieldMetadataSettingsMapping,
|
||||
type FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
type AddressSettings = FieldMetadataSettingsMapping[FieldMetadataType.ADDRESS];
|
||||
|
||||
export const getEnabledAddressSubFields = (
|
||||
settings: FieldMetadataItem['settings'] | null | undefined,
|
||||
): readonly AllowedAddressSubField[] => {
|
||||
const addressSettings = settings as AddressSettings | null | undefined;
|
||||
if (addressSettings?.subFields && addressSettings.subFields.length > 0) {
|
||||
return addressSettings.subFields;
|
||||
}
|
||||
return DEFAULT_VISIBLE_ADDRESS_SUBFIELDS;
|
||||
};
|
||||
+20
-4
@@ -1,5 +1,7 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { getDefaultSortSubFieldForAddress } from '@/object-metadata/utils/getDefaultSortSubFieldForAddress';
|
||||
import { getDefaultSortSubFieldForFullName } from '@/object-metadata/utils/getDefaultSortSubFieldForFullName';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
|
||||
import {
|
||||
@@ -14,19 +16,33 @@ import {
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const getOrderByForFieldMetadataType = (
|
||||
field: Pick<FieldMetadataItem, 'id' | 'name' | 'type'>,
|
||||
field: Pick<FieldMetadataItem, 'id' | 'name' | 'type' | 'settings'>,
|
||||
direction: OrderBy | null | undefined,
|
||||
): RecordGqlOperationOrderBy => {
|
||||
switch (field.type) {
|
||||
case FieldMetadataType.FULL_NAME:
|
||||
case FieldMetadataType.FULL_NAME: {
|
||||
const primarySubField = getDefaultSortSubFieldForFullName(field.settings);
|
||||
const secondarySubField =
|
||||
primarySubField === 'firstName' ? 'lastName' : 'firstName';
|
||||
return [
|
||||
{
|
||||
[field.name]: {
|
||||
firstName: direction ?? 'AscNullsLast',
|
||||
lastName: direction ?? 'AscNullsLast',
|
||||
[primarySubField]: direction ?? 'AscNullsLast',
|
||||
[secondarySubField]: direction ?? 'AscNullsLast',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
case FieldMetadataType.ADDRESS: {
|
||||
const subField = getDefaultSortSubFieldForAddress(field.settings);
|
||||
return [
|
||||
{
|
||||
[field.name]: {
|
||||
[subField]: direction ?? 'AscNullsLast',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
case FieldMetadataType.CURRENCY:
|
||||
return [
|
||||
{
|
||||
|
||||
+5
-1
@@ -5,6 +5,7 @@ import { type CurrencyCode } from 'twenty-shared/constants';
|
||||
import {
|
||||
ConnectedAccountProvider,
|
||||
type AllowedAddressSubField,
|
||||
type AllowedFullNameSubField,
|
||||
type FieldMetadataMultiItemSettings,
|
||||
type FileCategory,
|
||||
} from 'twenty-shared/types';
|
||||
@@ -98,7 +99,9 @@ export type FieldCurrencyMetadata = BaseFieldMetadata & {
|
||||
|
||||
export type FieldFullNameMetadata = BaseFieldMetadata & {
|
||||
placeHolder: string;
|
||||
settings?: null;
|
||||
settings?: {
|
||||
defaultSortSubField?: AllowedFullNameSubField | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type FieldEmailMetadata = BaseFieldMetadata & {
|
||||
@@ -123,6 +126,7 @@ export type FieldAddressMetadata = BaseFieldMetadata & {
|
||||
placeHolder: string;
|
||||
settings?: {
|
||||
subFields?: AllowedAddressSubField[] | null;
|
||||
defaultSortSubField?: AllowedAddressSubField | null;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -25,4 +25,5 @@ export const addressSettingsSchema = z.object({
|
||||
.min(1)
|
||||
.optional()
|
||||
.nullable(),
|
||||
defaultSortSubField: z.enum(ALLOWED_ADDRESS_SUBFIELDS).optional().nullable(),
|
||||
});
|
||||
|
||||
+77
-2
@@ -1,6 +1,7 @@
|
||||
import { Separator } from '@/settings/components/Separator';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { getDefaultSortSubFieldForAddress } from '@/object-metadata/utils/getDefaultSortSubFieldForAddress';
|
||||
import {
|
||||
addressSchema as addressFieldDefaultValueSchema,
|
||||
addressSettingsSchema,
|
||||
@@ -13,8 +14,13 @@ import { useCountries } from '@/ui/input/components/internal/hooks/useCountries'
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type MouseEvent } from 'react';
|
||||
import { useEffect, type MouseEvent } from 'react';
|
||||
import {
|
||||
DEFAULT_VISIBLE_ADDRESS_SUBFIELDS,
|
||||
type AllowedAddressSubField,
|
||||
} from 'twenty-shared/types';
|
||||
import {
|
||||
IconArrowsSort,
|
||||
IconCircleOff,
|
||||
IconList,
|
||||
IconMap,
|
||||
@@ -46,7 +52,10 @@ export const SettingsDataModelFieldAddressForm = ({
|
||||
existingFieldMetadataId,
|
||||
}: SettingsDataModelFieldAddressFormProps) => {
|
||||
const { t } = useLingui();
|
||||
const { control } = useFormContext<SettingsDataModelFieldTextFormValues>();
|
||||
const { control, watch, setValue } =
|
||||
useFormContext<SettingsDataModelFieldTextFormValues>();
|
||||
const watchedSubFields = watch('settings.subFields');
|
||||
const watchedDefaultSortSubField = watch('settings.defaultSortSubField');
|
||||
const countries = [
|
||||
{
|
||||
label: t`No country`,
|
||||
@@ -65,6 +74,7 @@ export const SettingsDataModelFieldAddressForm = ({
|
||||
const {
|
||||
initialDisplaySubFields,
|
||||
initialDefaultValue,
|
||||
initialDefaultSortSubField,
|
||||
resetDefaultValueField,
|
||||
} = useAddressSettingsFormInitialValues({ existingFieldMetadataId });
|
||||
|
||||
@@ -74,6 +84,37 @@ export const SettingsDataModelFieldAddressForm = ({
|
||||
closeDropdown('addressSubFieldsId');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const enabledSubFieldSet = new Set<string>(
|
||||
watchedSubFields ?? initialDisplaySubFields,
|
||||
);
|
||||
const resolvedSubField = getDefaultSortSubFieldForAddress({
|
||||
subFields: DEFAULT_VISIBLE_ADDRESS_SUBFIELDS.filter((subField) =>
|
||||
enabledSubFieldSet.has(subField),
|
||||
),
|
||||
defaultSortSubField: watchedDefaultSortSubField,
|
||||
});
|
||||
if (resolvedSubField !== watchedDefaultSortSubField) {
|
||||
setValue('settings.defaultSortSubField', resolvedSubField);
|
||||
}
|
||||
}, [
|
||||
watchedSubFields,
|
||||
watchedDefaultSortSubField,
|
||||
initialDisplaySubFields,
|
||||
setValue,
|
||||
]);
|
||||
|
||||
const subFieldLabels: Record<AllowedAddressSubField, string> = {
|
||||
addressStreet1: t`Address 1`,
|
||||
addressStreet2: t`Address 2`,
|
||||
addressCity: t`City`,
|
||||
addressState: t`State`,
|
||||
addressPostcode: t`Postcode`,
|
||||
addressCountry: t`Country`,
|
||||
addressLat: t`Latitude`,
|
||||
addressLng: t`Longitude`,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
@@ -144,6 +185,40 @@ export const SettingsDataModelFieldAddressForm = ({
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Separator />
|
||||
<Controller
|
||||
name="settings.defaultSortSubField"
|
||||
defaultValue={initialDefaultSortSubField}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const enabledSubFieldSet = new Set<string>(
|
||||
watchedSubFields ?? initialDisplaySubFields,
|
||||
);
|
||||
const sortOptions: SelectOption<AllowedAddressSubField>[] =
|
||||
DEFAULT_VISIBLE_ADDRESS_SUBFIELDS.filter((subField) =>
|
||||
enabledSubFieldSet.has(subField),
|
||||
).map((subField) => ({
|
||||
label: subFieldLabels[subField],
|
||||
value: subField,
|
||||
}));
|
||||
return (
|
||||
<SettingsOptionCardContentSelect
|
||||
Icon={IconArrowsSort}
|
||||
title={t`Default Sort Sub-Field`}
|
||||
description={t`Pick which sub-field is used when sorting this column`}
|
||||
>
|
||||
<Select<AllowedAddressSubField>
|
||||
dropdownId="addressDefaultSortSubFieldId"
|
||||
disabled={disabled || sortOptions.length === 0}
|
||||
value={value ?? initialDefaultSortSubField}
|
||||
onChange={onChange}
|
||||
options={sortOptions}
|
||||
selectSizeVariant="small"
|
||||
/>
|
||||
</SettingsOptionCardContentSelect>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+15
-27
@@ -1,32 +1,20 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type AllowedAddressSubField } from 'twenty-shared/types';
|
||||
import {
|
||||
DEFAULT_VISIBLE_ADDRESS_SUBFIELDS,
|
||||
type AllowedAddressSubField,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
export const DEFAULT_SELECTION_ADDRESS_WITH_MESSAGES: {
|
||||
value: AllowedAddressSubField;
|
||||
label: ReturnType<typeof msg>;
|
||||
}[] = [
|
||||
{
|
||||
value: 'addressStreet1',
|
||||
label: msg`Address 1`,
|
||||
},
|
||||
{
|
||||
value: 'addressStreet2',
|
||||
label: msg`Address 2`,
|
||||
},
|
||||
{
|
||||
value: 'addressCity',
|
||||
label: msg`City`,
|
||||
},
|
||||
{
|
||||
value: 'addressState',
|
||||
label: msg`State`,
|
||||
},
|
||||
{
|
||||
value: 'addressPostcode',
|
||||
label: msg`Postcode`,
|
||||
},
|
||||
{
|
||||
value: 'addressCountry',
|
||||
label: msg`Country`,
|
||||
},
|
||||
];
|
||||
}[] = DEFAULT_VISIBLE_ADDRESS_SUBFIELDS.map((value) => ({
|
||||
value,
|
||||
label: {
|
||||
addressStreet1: msg`Address 1`,
|
||||
addressStreet2: msg`Address 2`,
|
||||
addressCity: msg`City`,
|
||||
addressState: msg`State`,
|
||||
addressPostcode: msg`Postcode`,
|
||||
addressCountry: msg`Country`,
|
||||
}[value],
|
||||
}));
|
||||
|
||||
+14
@@ -153,6 +153,20 @@ describe('useAddressSettingsFormInitialValues', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fall back to addressCity as the initial default sort sub-field when no settings are configured', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAddressSettingsFormInitialValues({
|
||||
existingFieldMetadataId: 'new-field',
|
||||
}),
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.initialDefaultSortSubField).toEqual('addressCity');
|
||||
});
|
||||
|
||||
it('should call resetField with all address subFields when resetDefaultValueField is called', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
|
||||
+6
@@ -1,4 +1,5 @@
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { getDefaultSortSubFieldForAddress } from '@/object-metadata/utils/getDefaultSortSubFieldForAddress';
|
||||
import { type SettingsDataModelFieldTextFormValues } from '@/settings/data-model/fields/forms/address/components/SettingsDataModelFieldAddressForm';
|
||||
import { DEFAULT_SELECTION_ADDRESS_WITH_MESSAGES } from '@/settings/data-model/fields/forms/address/constants/DefaultSelectionAddressWithMessages';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
@@ -23,6 +24,10 @@ export const useAddressSettingsFormInitialValues = ({
|
||||
? fieldMetadataItem.settings.subFields
|
||||
: allAddressSubFields;
|
||||
|
||||
const initialDefaultSortSubField = getDefaultSortSubFieldForAddress(
|
||||
fieldMetadataItem?.settings,
|
||||
);
|
||||
|
||||
const defaultDefaultValue = {
|
||||
addressStreet1: "''",
|
||||
addressStreet2: null,
|
||||
@@ -47,6 +52,7 @@ export const useAddressSettingsFormInitialValues = ({
|
||||
return {
|
||||
initialDefaultValue,
|
||||
initialDisplaySubFields,
|
||||
initialDefaultSortSubField,
|
||||
resetDefaultValueField,
|
||||
};
|
||||
};
|
||||
|
||||
+18
@@ -15,6 +15,8 @@ import { settingsDataModelFieldCurrencyFormSchema } from '@/settings/data-model/
|
||||
import { SettingsDataModelFieldCurrencySettingsFormCard } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard';
|
||||
import { settingsDataModelFieldDateFormSchema } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm';
|
||||
import { SettingsDataModelFieldDateSettingsFormCard } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard';
|
||||
import { settingsDataModelFieldFullNameFormSchema } from '@/settings/data-model/fields/forms/full-name/components/SettingsDataModelFieldFullNameForm';
|
||||
import { SettingsDataModelFieldFullNameSettingsFormCard } from '@/settings/data-model/fields/forms/full-name/components/SettingsDataModelFieldFullNameSettingsFormCard';
|
||||
import { settingsDataModelFieldMorphRelationFormSchema } from '@/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationForm';
|
||||
import { settingsDataModelFieldNumberFormSchema } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm';
|
||||
import { SettingsDataModelFieldNumberSettingsFormCard } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberSettingsFormCard';
|
||||
@@ -93,6 +95,10 @@ const addressFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.ADDRESS) })
|
||||
.extend(settingsDataModelFieldAddressFormSchema.shape);
|
||||
|
||||
const fullNameFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.FULL_NAME) })
|
||||
.extend(settingsDataModelFieldFullNameFormSchema.shape);
|
||||
|
||||
const phonesFieldFormSchema = z
|
||||
.object({ type: z.literal(FieldMetadataType.PHONES) })
|
||||
.extend(settingsDataModelFieldPhonesFormSchema.shape)
|
||||
@@ -142,6 +148,7 @@ const otherFieldsFormSchema = z
|
||||
FieldMetadataType.DATE_TIME,
|
||||
FieldMetadataType.NUMBER,
|
||||
FieldMetadataType.ADDRESS,
|
||||
FieldMetadataType.FULL_NAME,
|
||||
FieldMetadataType.PHONES,
|
||||
FieldMetadataType.TEXT,
|
||||
FieldMetadataType.EMAILS,
|
||||
@@ -168,6 +175,7 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion(
|
||||
numberFieldFormSchema,
|
||||
textFieldFormSchema,
|
||||
addressFieldFormSchema,
|
||||
fullNameFieldFormSchema,
|
||||
phonesFieldFormSchema,
|
||||
emailsFieldFormSchema,
|
||||
linksFieldFormSchema,
|
||||
@@ -297,6 +305,16 @@ export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldType === FieldMetadataType.FULL_NAME) {
|
||||
return (
|
||||
<SettingsDataModelFieldFullNameSettingsFormCard
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldType === FieldMetadataType.PHONES) {
|
||||
return (
|
||||
<SettingsDataModelFieldPhonesSettingsFormCard
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
ALLOWED_FULL_NAME_SUBFIELDS,
|
||||
type AllowedFullNameSubField,
|
||||
} from 'twenty-shared/types';
|
||||
import { IconArrowsSort } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { useFullNameSettingsFormInitialValues } from '@/settings/data-model/fields/forms/full-name/hooks/useFullNameSettingsFormInitialValues';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
export const fullNameSettingsSchema = z.object({
|
||||
defaultSortSubField: z
|
||||
.enum(ALLOWED_FULL_NAME_SUBFIELDS)
|
||||
.optional()
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export const settingsDataModelFieldFullNameFormSchema = z.object({
|
||||
settings: fullNameSettingsSchema,
|
||||
});
|
||||
|
||||
export type SettingsDataModelFieldFullNameFormValues = z.infer<
|
||||
typeof settingsDataModelFieldFullNameFormSchema
|
||||
>;
|
||||
|
||||
type SettingsDataModelFieldFullNameFormProps = {
|
||||
disabled?: boolean;
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldFullNameForm = ({
|
||||
disabled,
|
||||
existingFieldMetadataId,
|
||||
}: SettingsDataModelFieldFullNameFormProps) => {
|
||||
const { t } = useLingui();
|
||||
const { control } =
|
||||
useFormContext<SettingsDataModelFieldFullNameFormValues>();
|
||||
const { initialDefaultSortSubField } = useFullNameSettingsFormInitialValues({
|
||||
existingFieldMetadataId,
|
||||
});
|
||||
|
||||
const subFieldLabels: Record<AllowedFullNameSubField, string> = {
|
||||
firstName: t`First Name`,
|
||||
lastName: t`Last Name`,
|
||||
};
|
||||
|
||||
const sortOptions: SelectOption<AllowedFullNameSubField>[] =
|
||||
ALLOWED_FULL_NAME_SUBFIELDS.map((subField) => ({
|
||||
label: subFieldLabels[subField],
|
||||
value: subField,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="settings.defaultSortSubField"
|
||||
defaultValue={initialDefaultSortSubField}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<SettingsOptionCardContentSelect
|
||||
Icon={IconArrowsSort}
|
||||
title={t`Default Sort Sub-Field`}
|
||||
description={t`Pick which sub-field is used when sorting this column`}
|
||||
>
|
||||
<Select<AllowedFullNameSubField>
|
||||
dropdownId="fullNameDefaultSortSubFieldId"
|
||||
disabled={disabled}
|
||||
value={value ?? initialDefaultSortSubField}
|
||||
onChange={onChange}
|
||||
options={sortOptions}
|
||||
selectSizeVariant="small"
|
||||
/>
|
||||
</SettingsOptionCardContentSelect>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import {
|
||||
SettingsDataModelFieldFullNameForm,
|
||||
type SettingsDataModelFieldFullNameFormValues,
|
||||
} from '@/settings/data-model/fields/forms/full-name/components/SettingsDataModelFieldFullNameForm';
|
||||
import { SettingsDataModelFieldPreviewWidget } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewWidget';
|
||||
import { type SettingsDataModelFieldEditFormValues } from '~/pages/settings/data-model/SettingsObjectFieldEdit';
|
||||
|
||||
type SettingsDataModelFieldFullNameSettingsFormCardProps = {
|
||||
disabled?: boolean;
|
||||
existingFieldMetadataId: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldFullNameSettingsFormCard = ({
|
||||
disabled,
|
||||
existingFieldMetadataId,
|
||||
objectNameSingular,
|
||||
}: SettingsDataModelFieldFullNameSettingsFormCardProps) => {
|
||||
const { watch } = useFormContext<
|
||||
SettingsDataModelFieldFullNameFormValues &
|
||||
SettingsDataModelFieldEditFormValues
|
||||
>();
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<SettingsDataModelFieldPreviewWidget
|
||||
fieldMetadataItem={{
|
||||
icon: watch('icon'),
|
||||
label: watch('label'),
|
||||
type: FieldMetadataType.FULL_NAME,
|
||||
settings: {
|
||||
defaultSortSubField: watch('settings.defaultSortSubField'),
|
||||
},
|
||||
}}
|
||||
objectNameSingular={objectNameSingular}
|
||||
/>
|
||||
}
|
||||
form={
|
||||
<SettingsDataModelFieldFullNameForm
|
||||
disabled={disabled}
|
||||
existingFieldMetadataId={existingFieldMetadataId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
||||
import { getDefaultSortSubFieldForFullName } from '@/object-metadata/utils/getDefaultSortSubFieldForFullName';
|
||||
|
||||
type UseFullNameSettingsFormInitialValuesProps = {
|
||||
existingFieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const useFullNameSettingsFormInitialValues = ({
|
||||
existingFieldMetadataId,
|
||||
}: UseFullNameSettingsFormInitialValuesProps) => {
|
||||
const { fieldMetadataItem } = useFieldMetadataItemById(
|
||||
existingFieldMetadataId,
|
||||
);
|
||||
|
||||
const initialDefaultSortSubField = getDefaultSortSubFieldForFullName(
|
||||
fieldMetadataItem?.settings,
|
||||
);
|
||||
|
||||
return {
|
||||
initialDefaultSortSubField,
|
||||
};
|
||||
};
|
||||
+10
-1
@@ -115,7 +115,6 @@ type NotDefinedSettings = {
|
||||
// oxlint-disable-next-line unused-imports/no-unused-vars
|
||||
type SettingsAssertions = [
|
||||
Expect<HasAllProperties<CurrencyFieldMetadata, NotDefinedSettings>>,
|
||||
Expect<HasAllProperties<FullNameFieldMetadata, NotDefinedSettings>>,
|
||||
Expect<HasAllProperties<RatingFieldMetadata, NotDefinedSettings>>,
|
||||
Expect<HasAllProperties<SelectFieldMetadata, NotDefinedSettings>>,
|
||||
Expect<HasAllProperties<MultiSelectFieldMetadata, NotDefinedSettings>>,
|
||||
@@ -205,6 +204,16 @@ type SettingsAssertions = [
|
||||
}
|
||||
>
|
||||
>,
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
FullNameFieldMetadata,
|
||||
{
|
||||
settings: JsonbProperty<
|
||||
FieldMetadataSettingsMapping[FieldMetadataType.FULL_NAME]
|
||||
>;
|
||||
}
|
||||
>
|
||||
>,
|
||||
|
||||
Expect<
|
||||
HasAllProperties<
|
||||
|
||||
@@ -10,3 +10,12 @@ export const ALLOWED_ADDRESS_SUBFIELDS = [
|
||||
] as const;
|
||||
|
||||
export type AllowedAddressSubField = (typeof ALLOWED_ADDRESS_SUBFIELDS)[number];
|
||||
|
||||
export const DEFAULT_VISIBLE_ADDRESS_SUBFIELDS = [
|
||||
'addressStreet1',
|
||||
'addressStreet2',
|
||||
'addressCity',
|
||||
'addressState',
|
||||
'addressPostcode',
|
||||
'addressCountry',
|
||||
] as const satisfies readonly AllowedAddressSubField[];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type AllowedAddressSubField } from '@/types/AddressFieldsType';
|
||||
import { type FieldMetadataMultiItemSettings } from '@/types/FieldMetadataMultiItemSettings';
|
||||
import { type FieldMetadataType } from '@/types/FieldMetadataType';
|
||||
import { type AllowedFullNameSubField } from '@/types/FullNameFieldsType';
|
||||
import { type IsExactly } from '@/types/IsExactly';
|
||||
import { type RelationOnDeleteAction } from '@/types/RelationOnDeleteAction.type';
|
||||
import { type RelationType } from '@/types/RelationType';
|
||||
@@ -49,6 +50,11 @@ type FieldMetadataRelationSettings = {
|
||||
|
||||
type FieldMetadataAddressSettings = {
|
||||
subFields?: AllowedAddressSubField[];
|
||||
defaultSortSubField?: AllowedAddressSubField;
|
||||
};
|
||||
|
||||
type FieldMetadataFullNameSettings = {
|
||||
defaultSortSubField?: AllowedFullNameSubField;
|
||||
};
|
||||
|
||||
type FieldMetadataFilesSettings = {
|
||||
@@ -67,6 +73,7 @@ export type FieldMetadataSettingsMapping = {
|
||||
[FieldMetadataType.TEXT]: FieldMetadataTextSettings | null;
|
||||
[FieldMetadataType.RELATION]: FieldMetadataRelationSettings;
|
||||
[FieldMetadataType.ADDRESS]: FieldMetadataAddressSettings | null;
|
||||
[FieldMetadataType.FULL_NAME]: FieldMetadataFullNameSettings | null;
|
||||
[FieldMetadataType.MORPH_RELATION]: FieldMetadataRelationSettings;
|
||||
[FieldMetadataType.TS_VECTOR]: FieldMetadataTsVectorSettings | null;
|
||||
[FieldMetadataType.PHONES]: FieldMetadataMultiItemSettings | null;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export const ALLOWED_FULL_NAME_SUBFIELDS = ['firstName', 'lastName'] as const;
|
||||
|
||||
export type AllowedFullNameSubField =
|
||||
(typeof ALLOWED_FULL_NAME_SUBFIELDS)[number];
|
||||
@@ -8,7 +8,10 @@
|
||||
*/
|
||||
|
||||
export type { AllowedAddressSubField } from './AddressFieldsType';
|
||||
export { ALLOWED_ADDRESS_SUBFIELDS } from './AddressFieldsType';
|
||||
export {
|
||||
ALLOWED_ADDRESS_SUBFIELDS,
|
||||
DEFAULT_VISIBLE_ADDRESS_SUBFIELDS,
|
||||
} from './AddressFieldsType';
|
||||
export { AggregateOperations } from './AggregateOperations';
|
||||
export { AppBasePath } from './AppBasePath';
|
||||
export { AppPath } from './AppPath';
|
||||
@@ -123,6 +126,8 @@ export { FILTERABLE_FIELD_TYPES } from './FilterableFieldType';
|
||||
export { FirstDayOfTheWeek } from './FirstDayOfTheWeek';
|
||||
export type { FormatRecordSerializedRelationProperties } from './FormatRecordSerializedRelationProperties.type';
|
||||
export type { FromTo } from './FromToType';
|
||||
export type { AllowedFullNameSubField } from './FullNameFieldsType';
|
||||
export { ALLOWED_FULL_NAME_SUBFIELDS } from './FullNameFieldsType';
|
||||
export { HTTPMethod } from './HttpMethod';
|
||||
export type { IndexOf } from './IndexOf.type';
|
||||
export type { IsEmptyObject } from './IsEmptyObject.type';
|
||||
|
||||
Reference in New Issue
Block a user