mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-26 13:42:44 +03:00
Introduce ARRAY field type (#6862)
This PR was created by \[GitStart\](<https://gitstart.com/>) to address the requirements from this ticket: \[TWNTY-6447\](<https://clients.gitstart.com/twenty/5449/tickets/TWNTY-6447>). This ticket was imported from: <https://github.com/twentyhq/twenty/issues/6447> ### Description \- We added a new field type ### Refs #6447 ### Demo <https://jam.dev/c/2b4d7853-ea89-4e9d-a561-6edcb4fdb34b> Fixes #6447 --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
parent
bc99cfec98
commit
8208a3e976
@ -376,7 +376,8 @@ export enum FieldMetadataType {
|
|||||||
RichText = 'RICH_TEXT',
|
RichText = 'RICH_TEXT',
|
||||||
Select = 'SELECT',
|
Select = 'SELECT',
|
||||||
Text = 'TEXT',
|
Text = 'TEXT',
|
||||||
Uuid = 'UUID'
|
Uuid = 'UUID',
|
||||||
|
Array = 'ARRAY'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FileFolder {
|
export enum FileFolder {
|
||||||
|
@ -260,6 +260,7 @@ export type FieldConnection = {
|
|||||||
export enum FieldMetadataType {
|
export enum FieldMetadataType {
|
||||||
Actor = 'ACTOR',
|
Actor = 'ACTOR',
|
||||||
Address = 'ADDRESS',
|
Address = 'ADDRESS',
|
||||||
|
Array = 'ARRAY',
|
||||||
Boolean = 'BOOLEAN',
|
Boolean = 'BOOLEAN',
|
||||||
Currency = 'CURRENCY',
|
Currency = 'CURRENCY',
|
||||||
Date = 'DATE',
|
Date = 'DATE',
|
||||||
|
@ -98,6 +98,8 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => {
|
|||||||
return 'RATING';
|
return 'RATING';
|
||||||
case FieldMetadataType.Actor:
|
case FieldMetadataType.Actor:
|
||||||
return 'ACTOR';
|
return 'ACTOR';
|
||||||
|
case FieldMetadataType.Array:
|
||||||
|
return 'ARRAY';
|
||||||
default:
|
default:
|
||||||
return 'TEXT';
|
return 'TEXT';
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
|
|||||||
FieldMetadataType.Position,
|
FieldMetadataType.Position,
|
||||||
FieldMetadataType.RawJson,
|
FieldMetadataType.RawJson,
|
||||||
FieldMetadataType.RichText,
|
FieldMetadataType.RichText,
|
||||||
|
FieldMetadataType.Array,
|
||||||
].includes(fieldType);
|
].includes(fieldType);
|
||||||
|
|
||||||
if (fieldIsSimpleValue) {
|
if (fieldIsSimpleValue) {
|
||||||
|
@ -67,6 +67,7 @@ export const MultipleFiltersDropdownContent = ({
|
|||||||
'LINKS',
|
'LINKS',
|
||||||
'ADDRESS',
|
'ADDRESS',
|
||||||
'ACTOR',
|
'ACTOR',
|
||||||
|
'ARRAY',
|
||||||
'PHONES',
|
'PHONES',
|
||||||
].includes(filterDefinitionUsedInDropdown.type) && (
|
].includes(filterDefinitionUsedInDropdown.type) && (
|
||||||
<ObjectFilterDropdownTextSearchInput />
|
<ObjectFilterDropdownTextSearchInput />
|
||||||
|
@ -16,4 +16,5 @@ export type FilterType =
|
|||||||
| 'SELECT'
|
| 'SELECT'
|
||||||
| 'RATING'
|
| 'RATING'
|
||||||
| 'MULTI_SELECT'
|
| 'MULTI_SELECT'
|
||||||
| 'ACTOR';
|
| 'ACTOR'
|
||||||
|
| 'ARRAY';
|
||||||
|
@ -22,6 +22,7 @@ export const getOperandsForFilterType = (
|
|||||||
case 'LINK':
|
case 'LINK':
|
||||||
case 'LINKS':
|
case 'LINKS':
|
||||||
case 'ACTOR':
|
case 'ACTOR':
|
||||||
|
case 'ARRAY':
|
||||||
case 'PHONES':
|
case 'PHONES':
|
||||||
return [
|
return [
|
||||||
ViewFilterOperand.Contains,
|
ViewFilterOperand.Contains,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
import { ActorFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ActorFieldDisplay';
|
import { ActorFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ActorFieldDisplay';
|
||||||
|
import { ArrayFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ArrayFieldDisplay';
|
||||||
import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
|
import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
|
||||||
import { EmailsFieldDisplay } from '@/object-record/record-field/meta-types/display/components/EmailsFieldDisplay';
|
import { EmailsFieldDisplay } from '@/object-record/record-field/meta-types/display/components/EmailsFieldDisplay';
|
||||||
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
||||||
@ -10,6 +11,7 @@ import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-
|
|||||||
import { RichTextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RichTextFieldDisplay';
|
import { RichTextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RichTextFieldDisplay';
|
||||||
import { isFieldIdentifierDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldIdentifierDisplay';
|
import { isFieldIdentifierDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldIdentifierDisplay';
|
||||||
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
|
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
|
||||||
|
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
||||||
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
||||||
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||||
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
|
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
|
||||||
@ -104,6 +106,8 @@ export const FieldDisplay = () => {
|
|||||||
<RichTextFieldDisplay />
|
<RichTextFieldDisplay />
|
||||||
) : isFieldActor(fieldDefinition) ? (
|
) : isFieldActor(fieldDefinition) ? (
|
||||||
<ActorFieldDisplay />
|
<ActorFieldDisplay />
|
||||||
|
) : isFieldArray(fieldDefinition) ? (
|
||||||
|
<ArrayFieldDisplay />
|
||||||
) : isFieldEmails(fieldDefinition) ? (
|
) : isFieldEmails(fieldDefinition) ? (
|
||||||
<EmailsFieldDisplay />
|
<EmailsFieldDisplay />
|
||||||
) : isFieldPhones(fieldDefinition) ? (
|
) : isFieldPhones(fieldDefinition) ? (
|
||||||
|
@ -24,7 +24,9 @@ import { isFieldRelationToOneObject } from '@/object-record/record-field/types/g
|
|||||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
|
|
||||||
|
import { ArrayFieldInput } from '@/object-record/record-field/meta-types/input/components/ArrayFieldInput';
|
||||||
import { RichTextFieldInput } from '@/object-record/record-field/meta-types/input/components/RichTextFieldInput';
|
import { RichTextFieldInput } from '@/object-record/record-field/meta-types/input/components/RichTextFieldInput';
|
||||||
|
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
||||||
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
|
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput';
|
import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput';
|
||||||
@ -187,6 +189,8 @@ export const FieldInput = ({
|
|||||||
/>
|
/>
|
||||||
) : isFieldRichText(fieldDefinition) ? (
|
) : isFieldRichText(fieldDefinition) ? (
|
||||||
<RichTextFieldInput />
|
<RichTextFieldInput />
|
||||||
|
) : isFieldArray(fieldDefinition) ? (
|
||||||
|
<ArrayFieldInput onCancel={onCancel} />
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
@ -26,6 +26,8 @@ import { isFieldSelectValue } from '@/object-record/record-field/types/guards/is
|
|||||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
|
|
||||||
|
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
||||||
|
import { isFieldArrayValue } from '@/object-record/record-field/types/guards/isFieldArrayValue';
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '../types/guards/isFieldBoolean';
|
||||||
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
|
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
|
||||||
@ -124,6 +126,9 @@ export const usePersistField = () => {
|
|||||||
isFieldRawJson(fieldDefinition) &&
|
isFieldRawJson(fieldDefinition) &&
|
||||||
isFieldRawJsonValue(valueToPersist);
|
isFieldRawJsonValue(valueToPersist);
|
||||||
|
|
||||||
|
const fieldIsArray =
|
||||||
|
isFieldArray(fieldDefinition) && isFieldArrayValue(valueToPersist);
|
||||||
|
|
||||||
const isValuePersistable =
|
const isValuePersistable =
|
||||||
fieldIsRelationToOneObject ||
|
fieldIsRelationToOneObject ||
|
||||||
fieldIsText ||
|
fieldIsText ||
|
||||||
@ -143,7 +148,8 @@ export const usePersistField = () => {
|
|||||||
fieldIsSelect ||
|
fieldIsSelect ||
|
||||||
fieldIsMultiSelect ||
|
fieldIsMultiSelect ||
|
||||||
fieldIsAddress ||
|
fieldIsAddress ||
|
||||||
fieldIsRawJson;
|
fieldIsRawJson ||
|
||||||
|
fieldIsArray;
|
||||||
|
|
||||||
if (isValuePersistable) {
|
if (isValuePersistable) {
|
||||||
const fieldName = fieldDefinition.metadata.fieldName;
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
import { THEME_COMMON } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||||
|
import { useArrayFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useArrayFieldDisplay';
|
||||||
|
import { ArrayDisplay } from '@/ui/field/display/components/ArrayDisplay';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const spacing1 = THEME_COMMON.spacing(1);
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: ${spacing1};
|
||||||
|
justify-content: flex-start;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ArrayFieldDisplay = () => {
|
||||||
|
const { fieldValue } = useArrayFieldDisplay();
|
||||||
|
|
||||||
|
const { isFocused } = useFieldFocus();
|
||||||
|
|
||||||
|
if (!Array.isArray(fieldValue)) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<ArrayDisplay value={fieldValue} isFocused={isFocused} />
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,44 @@
|
|||||||
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
|
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||||
|
import { FieldArrayValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
|
||||||
|
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
||||||
|
import { arraySchema } from '@/object-record/record-field/types/guards/isFieldArrayValue';
|
||||||
|
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const useArrayField = () => {
|
||||||
|
const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
||||||
|
|
||||||
|
assertFieldMetadata(FieldMetadataType.Array, isFieldArray, fieldDefinition);
|
||||||
|
|
||||||
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
|
|
||||||
|
const [fieldValue, setFieldValue] = useRecoilState<FieldArrayValue>(
|
||||||
|
recordStoreFamilySelector({
|
||||||
|
recordId,
|
||||||
|
fieldName,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const persistField = usePersistField();
|
||||||
|
|
||||||
|
const persistArrayField = (nextValue: string[]) => {
|
||||||
|
if (!nextValue) persistField(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
persistField(arraySchema.parse(nextValue));
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
fieldValue,
|
||||||
|
setFieldValue,
|
||||||
|
persistArrayField,
|
||||||
|
hotkeyScope,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
|
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||||
|
import {
|
||||||
|
FieldArrayMetadata,
|
||||||
|
FieldArrayValue,
|
||||||
|
} from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
export const useArrayFieldDisplay = () => {
|
||||||
|
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||||
|
|
||||||
|
const { fieldName } = fieldDefinition.metadata;
|
||||||
|
|
||||||
|
const fieldValue = useRecordFieldValue<FieldArrayValue | undefined>(
|
||||||
|
recordId,
|
||||||
|
fieldName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fieldDefinition: fieldDefinition as FieldDefinition<FieldArrayMetadata>,
|
||||||
|
fieldValue,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,39 @@
|
|||||||
|
import { useArrayField } from '@/object-record/record-field/meta-types/hooks/useArrayField';
|
||||||
|
import { ArrayFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/ArrayFieldMenuItem';
|
||||||
|
import { MultiItemFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiItemFieldInput';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
type ArrayFieldInputProps = {
|
||||||
|
onCancel?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ArrayFieldInput = ({ onCancel }: ArrayFieldInputProps) => {
|
||||||
|
const { persistArrayField, hotkeyScope, fieldValue } = useArrayField();
|
||||||
|
|
||||||
|
const arrayItems = useMemo<Array<string>>(
|
||||||
|
() => (Array.isArray(fieldValue) ? fieldValue : []),
|
||||||
|
[fieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiItemFieldInput
|
||||||
|
hotkeyScope={hotkeyScope}
|
||||||
|
newItemLabel="Add Item"
|
||||||
|
items={arrayItems}
|
||||||
|
onPersist={persistArrayField}
|
||||||
|
onCancel={onCancel}
|
||||||
|
placeholder="Enter value"
|
||||||
|
fieldMetadataType={FieldMetadataType.Array}
|
||||||
|
renderItem={({ value, index, handleEdit, handleDelete }) => (
|
||||||
|
<ArrayFieldMenuItem
|
||||||
|
key={index}
|
||||||
|
dropdownId={`${hotkeyScope}-array-${index}`}
|
||||||
|
value={value}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
></MultiItemFieldInput>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,27 @@
|
|||||||
|
import { MultiItemFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/MultiItemFieldMenuItem';
|
||||||
|
import { ArrayDisplay } from '@/ui/field/display/components/ArrayDisplay';
|
||||||
|
|
||||||
|
type ArrayFieldMenuItemProps = {
|
||||||
|
dropdownId: string;
|
||||||
|
onEdit?: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ArrayFieldMenuItem = ({
|
||||||
|
dropdownId,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
value,
|
||||||
|
}: ArrayFieldMenuItemProps) => {
|
||||||
|
return (
|
||||||
|
<MultiItemFieldMenuItem
|
||||||
|
dropdownId={dropdownId}
|
||||||
|
value={value}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
|
DisplayComponent={() => <ArrayDisplay value={[value]} isInputDisplay />}
|
||||||
|
hasPrimaryButton={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -40,10 +40,12 @@ type MultiItemFieldInputProps<T> = {
|
|||||||
handleDelete: () => void;
|
handleDelete: () => void;
|
||||||
}) => React.ReactNode;
|
}) => React.ReactNode;
|
||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
|
newItemLabel?: string;
|
||||||
fieldMetadataType: FieldMetadataType;
|
fieldMetadataType: FieldMetadataType;
|
||||||
renderInput?: DropdownMenuInputProps['renderInput'];
|
renderInput?: DropdownMenuInputProps['renderInput'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Todo: the API of this component does not look healthy: we have renderInput, renderItem, formatInput, ...
|
||||||
export const MultiItemFieldInput = <T,>({
|
export const MultiItemFieldInput = <T,>({
|
||||||
items,
|
items,
|
||||||
onPersist,
|
onPersist,
|
||||||
@ -53,6 +55,7 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
formatInput,
|
formatInput,
|
||||||
renderItem,
|
renderItem,
|
||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
|
newItemLabel,
|
||||||
fieldMetadataType,
|
fieldMetadataType,
|
||||||
renderInput,
|
renderInput,
|
||||||
}: MultiItemFieldInputProps<T>) => {
|
}: MultiItemFieldInputProps<T>) => {
|
||||||
@ -181,7 +184,7 @@ export const MultiItemFieldInput = <T,>({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleAddButtonClick}
|
onClick={handleAddButtonClick}
|
||||||
LeftIcon={IconPlus}
|
LeftIcon={IconPlus}
|
||||||
text={`Add ${placeholder}`}
|
text={newItemLabel || `Add ${placeholder}`}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
)}
|
)}
|
||||||
|
@ -21,6 +21,7 @@ type MultiItemFieldMenuItemProps<T> = {
|
|||||||
onSetAsPrimary?: () => void;
|
onSetAsPrimary?: () => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
DisplayComponent: React.ComponentType<{ value: T }>;
|
DisplayComponent: React.ComponentType<{ value: T }>;
|
||||||
|
hasPrimaryButton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledIconBookmark = styled(IconBookmark)`
|
const StyledIconBookmark = styled(IconBookmark)`
|
||||||
@ -37,6 +38,7 @@ export const MultiItemFieldMenuItem = <T,>({
|
|||||||
onSetAsPrimary,
|
onSetAsPrimary,
|
||||||
onDelete,
|
onDelete,
|
||||||
DisplayComponent,
|
DisplayComponent,
|
||||||
|
hasPrimaryButton = true,
|
||||||
}: MultiItemFieldMenuItemProps<T>) => {
|
}: MultiItemFieldMenuItemProps<T>) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const { isDropdownOpen, closeDropdown } = useDropdown(dropdownId);
|
const { isDropdownOpen, closeDropdown } = useDropdown(dropdownId);
|
||||||
@ -74,7 +76,7 @@ export const MultiItemFieldMenuItem = <T,>({
|
|||||||
clickableComponent={iconButton}
|
clickableComponent={iconButton}
|
||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
{!isPrimary && (
|
{hasPrimaryButton && !isPrimary && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
LeftIcon={IconBookmarkPlus}
|
LeftIcon={IconBookmarkPlus}
|
||||||
text="Set as Primary"
|
text="Set as Primary"
|
||||||
|
@ -139,6 +139,12 @@ export type FieldActorMetadata = {
|
|||||||
fieldName: string;
|
fieldName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FieldArrayMetadata = {
|
||||||
|
objectMetadataNameSingular?: string;
|
||||||
|
fieldName: string;
|
||||||
|
values: { label: string; value: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
export type FieldPhonesMetadata = {
|
export type FieldPhonesMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
@ -161,7 +167,8 @@ export type FieldMetadata =
|
|||||||
| FieldTextMetadata
|
| FieldTextMetadata
|
||||||
| FieldUuidMetadata
|
| FieldUuidMetadata
|
||||||
| FieldAddressMetadata
|
| FieldAddressMetadata
|
||||||
| FieldActorMetadata;
|
| FieldActorMetadata
|
||||||
|
| FieldArrayMetadata;
|
||||||
|
|
||||||
export type FieldTextValue = string;
|
export type FieldTextValue = string;
|
||||||
export type FieldUUidValue = string;
|
export type FieldUUidValue = string;
|
||||||
@ -218,6 +225,8 @@ export type FieldActorValue = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FieldArrayValue = string[];
|
||||||
|
|
||||||
export type PhoneRecord = { number: string; countryCode: string };
|
export type PhoneRecord = { number: string; countryCode: string };
|
||||||
|
|
||||||
export type FieldPhonesValue = {
|
export type FieldPhonesValue = {
|
||||||
|
@ -4,6 +4,7 @@ import { FieldDefinition } from '../FieldDefinition';
|
|||||||
import {
|
import {
|
||||||
FieldActorMetadata,
|
FieldActorMetadata,
|
||||||
FieldAddressMetadata,
|
FieldAddressMetadata,
|
||||||
|
FieldArrayMetadata,
|
||||||
FieldBooleanMetadata,
|
FieldBooleanMetadata,
|
||||||
FieldCurrencyMetadata,
|
FieldCurrencyMetadata,
|
||||||
FieldDateMetadata,
|
FieldDateMetadata,
|
||||||
@ -56,23 +57,25 @@ type AssertFieldMetadataFunction = <
|
|||||||
? FieldNumberMetadata
|
? FieldNumberMetadata
|
||||||
: E extends 'PHONE'
|
: E extends 'PHONE'
|
||||||
? FieldPhoneMetadata
|
? FieldPhoneMetadata
|
||||||
: E extends 'PHONES'
|
: E extends 'RELATION'
|
||||||
? FieldPhonesMetadata
|
? FieldRelationMetadata
|
||||||
: E extends 'RELATION'
|
: E extends 'TEXT'
|
||||||
? FieldRelationMetadata
|
? FieldTextMetadata
|
||||||
: E extends 'TEXT'
|
: E extends 'UUID'
|
||||||
? FieldTextMetadata
|
? FieldUuidMetadata
|
||||||
: E extends 'UUID'
|
: E extends 'ADDRESS'
|
||||||
? FieldUuidMetadata
|
? FieldAddressMetadata
|
||||||
: E extends 'ADDRESS'
|
: E extends 'RAW_JSON'
|
||||||
? FieldAddressMetadata
|
? FieldRawJsonMetadata
|
||||||
: E extends 'RAW_JSON'
|
: E extends 'RICH_TEXT'
|
||||||
? FieldRawJsonMetadata
|
? FieldTextMetadata
|
||||||
: E extends 'RICH_TEXT'
|
: E extends 'ACTOR'
|
||||||
? FieldTextMetadata
|
? FieldActorMetadata
|
||||||
: E extends 'ACTOR'
|
: E extends 'ARRAY'
|
||||||
? FieldActorMetadata
|
? FieldArrayMetadata
|
||||||
: never,
|
: E extends 'PHONES'
|
||||||
|
? FieldPhonesMetadata
|
||||||
|
: never,
|
||||||
>(
|
>(
|
||||||
fieldType: E,
|
fieldType: E,
|
||||||
fieldTypeGuard: (
|
fieldTypeGuard: (
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
import { FieldDefinition } from '../FieldDefinition';
|
||||||
|
import { FieldArrayMetadata, FieldMetadata } from '../FieldMetadata';
|
||||||
|
|
||||||
|
export const isFieldArray = (
|
||||||
|
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
|
||||||
|
): field is FieldDefinition<FieldArrayMetadata> =>
|
||||||
|
field.type === FieldMetadataType.Array;
|
@ -0,0 +1,8 @@
|
|||||||
|
import { FieldArrayValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const arraySchema = z.union([z.null(), z.array(z.string())]);
|
||||||
|
|
||||||
|
export const isFieldArrayValue = (
|
||||||
|
fieldValue: unknown,
|
||||||
|
): fieldValue is FieldArrayValue => arraySchema.safeParse(fieldValue).success;
|
@ -10,6 +10,7 @@ import { isFieldPhones } from '@/object-record/record-field/types/guards/isField
|
|||||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
|
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
||||||
import { isFieldEmail } from '../types/guards/isFieldEmail';
|
import { isFieldEmail } from '../types/guards/isFieldEmail';
|
||||||
import { isFieldLink } from '../types/guards/isFieldLink';
|
import { isFieldLink } from '../types/guards/isFieldLink';
|
||||||
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
||||||
@ -33,6 +34,7 @@ export const getFieldButtonIcon = (
|
|||||||
'workspaceMember') ||
|
'workspaceMember') ||
|
||||||
isFieldLinks(fieldDefinition) ||
|
isFieldLinks(fieldDefinition) ||
|
||||||
isFieldEmails(fieldDefinition) ||
|
isFieldEmails(fieldDefinition) ||
|
||||||
|
isFieldArray(fieldDefinition) ||
|
||||||
isFieldPhones(fieldDefinition)
|
isFieldPhones(fieldDefinition)
|
||||||
) {
|
) {
|
||||||
return IconPencil;
|
return IconPencil;
|
||||||
|
@ -6,6 +6,8 @@ import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldA
|
|||||||
import { isFieldActorValue } from '@/object-record/record-field/types/guards/isFieldActorValue';
|
import { isFieldActorValue } from '@/object-record/record-field/types/guards/isFieldActorValue';
|
||||||
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
||||||
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
|
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
|
||||||
|
import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
|
||||||
|
import { isFieldArrayValue } from '@/object-record/record-field/types/guards/isFieldArrayValue';
|
||||||
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
||||||
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
|
import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency';
|
||||||
import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue';
|
import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue';
|
||||||
@ -76,8 +78,9 @@ export const isFieldValueEmpty = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFieldMultiSelect(fieldDefinition)) {
|
if (isFieldMultiSelect(fieldDefinition) || isFieldArray(fieldDefinition)) {
|
||||||
return (
|
return (
|
||||||
|
!isFieldArrayValue(fieldValue) ||
|
||||||
!isFieldMultiSelectValue(fieldValue, selectOptionValues) ||
|
!isFieldMultiSelectValue(fieldValue, selectOptionValues) ||
|
||||||
!isDefined(fieldValue)
|
!isDefined(fieldValue)
|
||||||
);
|
);
|
||||||
|
@ -83,6 +83,9 @@ export const generateEmptyFieldValue = (
|
|||||||
case FieldMetadataType.MultiSelect: {
|
case FieldMetadataType.MultiSelect: {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case FieldMetadataType.Array: {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
case FieldMetadataType.RawJson: {
|
case FieldMetadataType.RawJson: {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
IconBracketsContain,
|
||||||
IconComponent,
|
IconComponent,
|
||||||
IllustrationIconCalendarEvent,
|
IllustrationIconCalendarEvent,
|
||||||
IllustrationIconCalendarTime,
|
IllustrationIconCalendarTime,
|
||||||
@ -183,6 +184,12 @@ export const SETTINGS_FIELD_TYPE_CONFIGS = {
|
|||||||
Icon: IllustrationIconSetting,
|
Icon: IllustrationIconSetting,
|
||||||
category: 'Basic',
|
category: 'Basic',
|
||||||
},
|
},
|
||||||
|
[FieldMetadataType.Array]: {
|
||||||
|
label: 'Array',
|
||||||
|
Icon: IconBracketsContain,
|
||||||
|
category: 'Basic',
|
||||||
|
exampleValue: ['value1', 'value2'],
|
||||||
|
},
|
||||||
} as const satisfies Record<
|
} as const satisfies Record<
|
||||||
SettingsSupportedFieldType,
|
SettingsSupportedFieldType,
|
||||||
SettingsFieldTypeConfig
|
SettingsFieldTypeConfig
|
||||||
|
@ -80,6 +80,7 @@ const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const previewableTypes = [
|
const previewableTypes = [
|
||||||
|
FieldMetadataType.Array,
|
||||||
FieldMetadataType.Address,
|
FieldMetadataType.Address,
|
||||||
FieldMetadataType.Boolean,
|
FieldMetadataType.Boolean,
|
||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
BORDER_COMMON,
|
||||||
|
OverflowingTextWithTooltip,
|
||||||
|
THEME_COMMON,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
|
import { FieldArrayValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
type ArrayDisplayProps = {
|
||||||
|
value: FieldArrayValue;
|
||||||
|
isFocused?: boolean;
|
||||||
|
isInputDisplay?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const themeSpacing = THEME_COMMON.spacingMultiplicator;
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: ${themeSpacing * 1}px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTag = styled.div<{ isInputDisplay?: boolean }>`
|
||||||
|
background-color: ${({ theme, isInputDisplay }) =>
|
||||||
|
isInputDisplay ? 'transparent' : theme.background.tertiary};
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
|
||||||
|
border-radius: ${BORDER_COMMON.radius.sm};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ArrayDisplay = ({
|
||||||
|
value,
|
||||||
|
isFocused,
|
||||||
|
isInputDisplay = false,
|
||||||
|
}: ArrayDisplayProps) => {
|
||||||
|
return isFocused ? (
|
||||||
|
<ExpandableList isChipCountDisplayed>
|
||||||
|
{value?.map((item, index) => (
|
||||||
|
<StyledTag key={index}>
|
||||||
|
<OverflowingTextWithTooltip text={item} />
|
||||||
|
</StyledTag>
|
||||||
|
))}
|
||||||
|
</ExpandableList>
|
||||||
|
) : (
|
||||||
|
<StyledContainer>
|
||||||
|
{value?.map((item, index) => (
|
||||||
|
<StyledTag key={index} isInputDisplay={isInputDisplay}>
|
||||||
|
<OverflowingTextWithTooltip text={item} />
|
||||||
|
</StyledTag>
|
||||||
|
))}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import { MouseEvent, useContext } from 'react';
|
|
||||||
import { styled } from '@linaria/react';
|
import { styled } from '@linaria/react';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { MouseEvent, useContext } from 'react';
|
||||||
import { FONT_COMMON, THEME_COMMON, ThemeContext } from 'twenty-ui';
|
import { FONT_COMMON, THEME_COMMON, ThemeContext } from 'twenty-ui';
|
||||||
|
|
||||||
type RoundedLinkProps = {
|
type RoundedLinkProps = {
|
||||||
|
@ -230,6 +230,13 @@ const fieldEmailsMock = {
|
|||||||
defaultValue: [{ primaryEmail: '', additionalEmails: {} }],
|
defaultValue: [{ primaryEmail: '', additionalEmails: {} }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fieldArrayMock = {
|
||||||
|
name: 'fieldArray',
|
||||||
|
type: FieldMetadataType.ARRAY,
|
||||||
|
isNullable: true,
|
||||||
|
defaultValue: null,
|
||||||
|
};
|
||||||
|
|
||||||
const fieldPhonesMock = {
|
const fieldPhonesMock = {
|
||||||
name: FIELD_PHONES_MOCK_NAME,
|
name: FIELD_PHONES_MOCK_NAME,
|
||||||
type: FieldMetadataType.PHONES,
|
type: FieldMetadataType.PHONES,
|
||||||
@ -267,6 +274,7 @@ export const fields = [
|
|||||||
fieldRawJsonMock,
|
fieldRawJsonMock,
|
||||||
fieldRichTextMock,
|
fieldRichTextMock,
|
||||||
fieldActorMock,
|
fieldActorMock,
|
||||||
|
fieldArrayMock,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const objectMetadataItemMock = {
|
export const objectMetadataItemMock = {
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import { GraphQLInputObjectType, GraphQLList, GraphQLString } from 'graphql';
|
||||||
|
|
||||||
|
export const ArrayFilterType = new GraphQLInputObjectType({
|
||||||
|
name: 'ArrayFilter',
|
||||||
|
fields: {
|
||||||
|
contains: { type: new GraphQLList(GraphQLString) },
|
||||||
|
contains_any: { type: new GraphQLList(GraphQLString) },
|
||||||
|
not_contains: { type: new GraphQLList(GraphQLString) },
|
||||||
|
},
|
||||||
|
});
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from './array-filter.input-type';
|
||||||
export * from './big-float-filter.input-type';
|
export * from './big-float-filter.input-type';
|
||||||
export * from './big-int-filter.input-type';
|
export * from './big-int-filter.input-type';
|
||||||
export * from './boolean-filter.input-type';
|
export * from './boolean-filter.input-type';
|
||||||
|
@ -18,6 +18,7 @@ import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadat
|
|||||||
|
|
||||||
import { OrderByDirectionType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/enum';
|
import { OrderByDirectionType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/enum';
|
||||||
import {
|
import {
|
||||||
|
ArrayFilterType,
|
||||||
BigFloatFilterType,
|
BigFloatFilterType,
|
||||||
BooleanFilterType,
|
BooleanFilterType,
|
||||||
DateFilterType,
|
DateFilterType,
|
||||||
@ -45,6 +46,8 @@ export interface TypeOptions<T = any> {
|
|||||||
isIdField?: boolean;
|
isIdField?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StringArrayScalarType = new GraphQLList(GraphQLString);
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TypeMapperService {
|
export class TypeMapperService {
|
||||||
mapToScalarType(
|
mapToScalarType(
|
||||||
@ -55,7 +58,6 @@ export class TypeMapperService {
|
|||||||
if (isIdField || settings?.isForeignKey) {
|
if (isIdField || settings?.isForeignKey) {
|
||||||
return GraphQLID;
|
return GraphQLID;
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeScalarMapping = new Map<FieldMetadataType, GraphQLScalarType>([
|
const typeScalarMapping = new Map<FieldMetadataType, GraphQLScalarType>([
|
||||||
[FieldMetadataType.UUID, UUIDScalarType],
|
[FieldMetadataType.UUID, UUIDScalarType],
|
||||||
[FieldMetadataType.TEXT, GraphQLString],
|
[FieldMetadataType.TEXT, GraphQLString],
|
||||||
@ -74,6 +76,10 @@ export class TypeMapperService {
|
|||||||
[FieldMetadataType.NUMERIC, BigFloatScalarType],
|
[FieldMetadataType.NUMERIC, BigFloatScalarType],
|
||||||
[FieldMetadataType.POSITION, PositionScalarType],
|
[FieldMetadataType.POSITION, PositionScalarType],
|
||||||
[FieldMetadataType.RAW_JSON, RawJSONScalar],
|
[FieldMetadataType.RAW_JSON, RawJSONScalar],
|
||||||
|
[
|
||||||
|
FieldMetadataType.ARRAY,
|
||||||
|
StringArrayScalarType as unknown as GraphQLScalarType,
|
||||||
|
],
|
||||||
[FieldMetadataType.RICH_TEXT, GraphQLString],
|
[FieldMetadataType.RICH_TEXT, GraphQLString],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -111,6 +117,7 @@ export class TypeMapperService {
|
|||||||
[FieldMetadataType.POSITION, FloatFilterType],
|
[FieldMetadataType.POSITION, FloatFilterType],
|
||||||
[FieldMetadataType.RAW_JSON, RawJsonFilterType],
|
[FieldMetadataType.RAW_JSON, RawJsonFilterType],
|
||||||
[FieldMetadataType.RICH_TEXT, StringFilterType],
|
[FieldMetadataType.RICH_TEXT, StringFilterType],
|
||||||
|
[FieldMetadataType.ARRAY, ArrayFilterType],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return typeFilterMapping.get(fieldMetadataType);
|
return typeFilterMapping.get(fieldMetadataType);
|
||||||
@ -135,6 +142,7 @@ export class TypeMapperService {
|
|||||||
[FieldMetadataType.POSITION, OrderByDirectionType],
|
[FieldMetadataType.POSITION, OrderByDirectionType],
|
||||||
[FieldMetadataType.RAW_JSON, OrderByDirectionType],
|
[FieldMetadataType.RAW_JSON, OrderByDirectionType],
|
||||||
[FieldMetadataType.RICH_TEXT, OrderByDirectionType],
|
[FieldMetadataType.RICH_TEXT, OrderByDirectionType],
|
||||||
|
[FieldMetadataType.ARRAY, OrderByDirectionType],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return typeOrderByMapping.get(fieldMetadataType);
|
return typeOrderByMapping.get(fieldMetadataType);
|
||||||
|
@ -31,6 +31,7 @@ export const mapFieldMetadataToGraphqlQuery = (
|
|||||||
FieldMetadataType.POSITION,
|
FieldMetadataType.POSITION,
|
||||||
FieldMetadataType.RAW_JSON,
|
FieldMetadataType.RAW_JSON,
|
||||||
FieldMetadataType.RICH_TEXT,
|
FieldMetadataType.RICH_TEXT,
|
||||||
|
FieldMetadataType.ARRAY,
|
||||||
].includes(fieldType);
|
].includes(fieldType);
|
||||||
|
|
||||||
if (fieldIsSimpleValue) {
|
if (fieldIsSimpleValue) {
|
||||||
|
@ -70,6 +70,12 @@ describe('computeSchemaComponents', () => {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'date',
|
format: 'date',
|
||||||
},
|
},
|
||||||
|
fieldArray: {
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
},
|
||||||
fieldBoolean: {
|
fieldBoolean: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
@ -246,6 +252,12 @@ describe('computeSchemaComponents', () => {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'date',
|
format: 'date',
|
||||||
},
|
},
|
||||||
|
fieldArray: {
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
},
|
||||||
fieldBoolean: {
|
fieldBoolean: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
@ -421,6 +433,12 @@ describe('computeSchemaComponents', () => {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'date',
|
format: 'date',
|
||||||
},
|
},
|
||||||
|
fieldArray: {
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
type: 'array',
|
||||||
|
},
|
||||||
fieldBoolean: {
|
fieldBoolean: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
|
@ -17,8 +17,8 @@ import {
|
|||||||
FieldMetadataType,
|
FieldMetadataType,
|
||||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import { capitalize } from 'src/utils/capitalize';
|
|
||||||
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
|
import { capitalize } from 'src/utils/capitalize';
|
||||||
|
|
||||||
type Property = OpenAPIV3_1.SchemaObject;
|
type Property = OpenAPIV3_1.SchemaObject;
|
||||||
|
|
||||||
@ -124,6 +124,14 @@ const getSchemaComponentsProperties = ({
|
|||||||
enum: field.options.map((option: { value: string }) => option.value),
|
enum: field.options.map((option: { value: string }) => option.value),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case FieldMetadataType.ARRAY:
|
||||||
|
itemProperty = {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
case FieldMetadataType.RATING:
|
case FieldMetadataType.RATING:
|
||||||
itemProperty = {
|
itemProperty = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -46,6 +46,7 @@ export enum FieldMetadataType {
|
|||||||
RAW_JSON = 'RAW_JSON',
|
RAW_JSON = 'RAW_JSON',
|
||||||
RICH_TEXT = 'RICH_TEXT',
|
RICH_TEXT = 'RICH_TEXT',
|
||||||
ACTOR = 'ACTOR',
|
ACTOR = 'ACTOR',
|
||||||
|
ARRAY = 'ARRAY',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity('fieldMetadata')
|
@Entity('fieldMetadata')
|
||||||
|
@ -29,7 +29,8 @@ export type BasicFieldMetadataType =
|
|||||||
| FieldMetadataType.POSITION
|
| FieldMetadataType.POSITION
|
||||||
| FieldMetadataType.DATE_TIME
|
| FieldMetadataType.DATE_TIME
|
||||||
| FieldMetadataType.DATE
|
| FieldMetadataType.DATE
|
||||||
| FieldMetadataType.POSITION;
|
| FieldMetadataType.POSITION
|
||||||
|
| FieldMetadataType.ARRAY;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicFieldMetadataType> {
|
export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicFieldMetadataType> {
|
||||||
@ -48,6 +49,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
|||||||
action: WorkspaceMigrationColumnActionType.CREATE,
|
action: WorkspaceMigrationColumnActionType.CREATE,
|
||||||
columnName,
|
columnName,
|
||||||
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
|
columnType: fieldMetadataTypeToColumnType(fieldMetadata.type),
|
||||||
|
isArray: fieldMetadata.type === FieldMetadataType.ARRAY,
|
||||||
isNullable: fieldMetadata.isNullable ?? true,
|
isNullable: fieldMetadata.isNullable ?? true,
|
||||||
defaultValue: serializedDefaultValue,
|
defaultValue: serializedDefaultValue,
|
||||||
},
|
},
|
||||||
@ -81,6 +83,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
|||||||
currentColumnDefinition: {
|
currentColumnDefinition: {
|
||||||
columnName: currentColumnName,
|
columnName: currentColumnName,
|
||||||
columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
|
columnType: fieldMetadataTypeToColumnType(currentFieldMetadata.type),
|
||||||
|
isArray: currentFieldMetadata.type === FieldMetadataType.ARRAY,
|
||||||
isNullable: currentFieldMetadata.isNullable ?? true,
|
isNullable: currentFieldMetadata.isNullable ?? true,
|
||||||
defaultValue: serializeDefaultValue(
|
defaultValue: serializeDefaultValue(
|
||||||
currentFieldMetadata.defaultValue,
|
currentFieldMetadata.defaultValue,
|
||||||
@ -89,6 +92,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
|||||||
alteredColumnDefinition: {
|
alteredColumnDefinition: {
|
||||||
columnName: alteredColumnName,
|
columnName: alteredColumnName,
|
||||||
columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type),
|
columnType: fieldMetadataTypeToColumnType(alteredFieldMetadata.type),
|
||||||
|
isArray: alteredFieldMetadata.type === FieldMetadataType.ARRAY,
|
||||||
isNullable: alteredFieldMetadata.isNullable ?? true,
|
isNullable: alteredFieldMetadata.isNullable ?? true,
|
||||||
defaultValue: serializedDefaultValue,
|
defaultValue: serializedDefaultValue,
|
||||||
},
|
},
|
||||||
|
@ -16,6 +16,7 @@ export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>(
|
|||||||
return 'uuid';
|
return 'uuid';
|
||||||
case FieldMetadataType.TEXT:
|
case FieldMetadataType.TEXT:
|
||||||
case FieldMetadataType.RICH_TEXT:
|
case FieldMetadataType.RICH_TEXT:
|
||||||
|
case FieldMetadataType.ARRAY:
|
||||||
return 'text';
|
return 'text';
|
||||||
case FieldMetadataType.PHONE:
|
case FieldMetadataType.PHONE:
|
||||||
case FieldMetadataType.EMAIL:
|
case FieldMetadataType.EMAIL:
|
||||||
|
@ -97,6 +97,7 @@ export class WorkspaceMigrationFactory {
|
|||||||
],
|
],
|
||||||
[FieldMetadataType.LINKS, { factory: this.compositeColumnActionFactory }],
|
[FieldMetadataType.LINKS, { factory: this.compositeColumnActionFactory }],
|
||||||
[FieldMetadataType.ACTOR, { factory: this.compositeColumnActionFactory }],
|
[FieldMetadataType.ACTOR, { factory: this.compositeColumnActionFactory }],
|
||||||
|
[FieldMetadataType.ARRAY, { factory: this.basicColumnActionFactory }],
|
||||||
[
|
[
|
||||||
FieldMetadataType.EMAILS,
|
FieldMetadataType.EMAILS,
|
||||||
{ factory: this.compositeColumnActionFactory },
|
{ factory: this.compositeColumnActionFactory },
|
||||||
|
@ -22,6 +22,7 @@ export {
|
|||||||
IconBookmark,
|
IconBookmark,
|
||||||
IconBookmarkPlus,
|
IconBookmarkPlus,
|
||||||
IconBox,
|
IconBox,
|
||||||
|
IconBracketsContain,
|
||||||
IconBrandGithub,
|
IconBrandGithub,
|
||||||
IconBrandGoogle,
|
IconBrandGoogle,
|
||||||
IconBrandLinkedin,
|
IconBrandLinkedin,
|
||||||
|
Loading…
Reference in New Issue
Block a user