Skip to content

Commit

Permalink
Introduce ARRAY field type
Browse files Browse the repository at this point in the history
  • Loading branch information
gitstart-twenty committed Sep 5, 2024
1 parent cddc92c commit 10d86ee
Show file tree
Hide file tree
Showing 41 changed files with 454 additions and 14 deletions.
3 changes: 2 additions & 1 deletion packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,8 @@ export enum FieldMetadataType {
RichText = 'RICH_TEXT',
Select = 'SELECT',
Text = 'TEXT',
Uuid = 'UUID'
Uuid = 'UUID',
Array = 'ARRAY'
}

export enum FileFolder {
Expand Down
3 changes: 2 additions & 1 deletion packages/twenty-front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
Expand Down Expand Up @@ -260,6 +260,7 @@ export type FieldConnection = {
export enum FieldMetadataType {
Actor = 'ACTOR',
Address = 'ADDRESS',
Array = 'ARRAY',
Boolean = 'BOOLEAN',
Currency = 'CURRENCY',
Date = 'DATE',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => {
return 'RATING';
case FieldMetadataType.Actor:
return 'ACTOR';
case FieldMetadataType.Array:
return 'ARRAY';
default:
return 'TEXT';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const mapFieldMetadataToGraphQLQuery = ({
FieldMetadataType.Position,
FieldMetadataType.RawJson,
FieldMetadataType.RichText,
FieldMetadataType.Array,
].includes(fieldType);

if (fieldIsSimpleValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const MultipleFiltersDropdownContent = ({
'LINKS',
'ADDRESS',
'ACTOR',
'ARRAY',
].includes(filterDefinitionUsedInDropdown.type) && (
<ObjectFilterDropdownTextSearchInput />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export type FilterType =
| 'SELECT'
| 'RATING'
| 'MULTI_SELECT'
| 'ACTOR';
| 'ACTOR'
| 'ARRAY';
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const getOperandsForFilterType = (
case 'LINK':
case 'LINKS':
case 'ACTOR':
case 'ARRAY':
return [
ViewFilterOperand.Contains,
ViewFilterOperand.DoesNotContain,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useContext } from 'react';

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 { EmailsFieldDisplay } from '@/object-record/record-field/meta-types/display/components/EmailsFieldDisplay';
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
Expand All @@ -9,6 +10,7 @@ import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-
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 { 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 { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails';
Expand Down Expand Up @@ -102,6 +104,8 @@ export const FieldDisplay = () => {
<RichTextFieldDisplay />
) : isFieldActor(fieldDefinition) ? (
<ActorFieldDisplay />
) : isFieldArray(fieldDefinition) ? (
<ArrayFieldDisplay />
) : isFieldEmails(fieldDefinition) ? (
<EmailsFieldDisplay />
) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import { isFieldRelationToOneObject } from '@/object-record/record-field/types/g
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
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 { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { FieldContext } from '../contexts/FieldContext';
import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput';
Expand Down Expand Up @@ -183,6 +185,8 @@ export const FieldInput = ({
/>
) : isFieldRichText(fieldDefinition) ? (
<RichTextFieldInput />
) : isFieldArray(fieldDefinition) ? (
<ArrayFieldInput onCancel={onCancel} />
) : (
<></>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { isFieldSelectValue } from '@/object-record/record-field/types/guards/is
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
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 { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldBooleanValue } from '../types/guards/isFieldBooleanValue';
Expand Down Expand Up @@ -119,6 +121,9 @@ export const usePersistField = () => {
isFieldRawJson(fieldDefinition) &&
isFieldRawJsonValue(valueToPersist);

const fieldIsArray =
isFieldArray(fieldDefinition) && isFieldArrayValue(valueToPersist);

const isValuePersistable =
fieldIsRelationToOneObject ||
fieldIsText ||
Expand All @@ -137,7 +142,8 @@ export const usePersistField = () => {
fieldIsSelect ||
fieldIsMultiSelect ||
fieldIsAddress ||
fieldIsRawJson;
fieldIsRawJson ||
fieldIsArray;

if (isValuePersistable) {
const fieldName = fieldDefinition.metadata.fieldName;
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
@@ -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,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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';

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"
renderItem={({ value, index, handleEdit, handleDelete }) => (
<ArrayFieldMenuItem
key={index}
dropdownId={`${hotkeyScope}-array-${index}`}
value={value}
onEdit={handleEdit}
onDelete={handleDelete}
/>
)}
></MultiItemFieldInput>
);
};
Original file line number Diff line number Diff line change
@@ -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}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type MultiItemFieldInputProps<T> = {
handleDelete: () => void;
}) => React.ReactNode;
hotkeyScope: string;
newItemLabel?: string;
};

export const MultiItemFieldInput = <T,>({
Expand All @@ -46,6 +47,7 @@ export const MultiItemFieldInput = <T,>({
formatInput,
renderItem,
hotkeyScope,
newItemLabel,
}: MultiItemFieldInputProps<T>) => {
const containerRef = useRef<HTMLDivElement>(null);
const handleDropdownClose = () => {
Expand Down Expand Up @@ -146,7 +148,7 @@ export const MultiItemFieldInput = <T,>({
<MenuItem
onClick={handleAddButtonClick}
LeftIcon={IconPlus}
text={`Add ${placeholder}`}
text={newItemLabel || `Add ${placeholder}`}
/>
</DropdownMenuItemsContainer>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type MultiItemFieldMenuItemProps<T> = {
onSetAsPrimary?: () => void;
onDelete?: () => void;
DisplayComponent: React.ComponentType<{ value: T }>;
hasPrimaryButton?: boolean;
};

const StyledIconBookmark = styled(IconBookmark)`
Expand All @@ -37,6 +38,7 @@ export const MultiItemFieldMenuItem = <T,>({
onSetAsPrimary,
onDelete,
DisplayComponent,
hasPrimaryButton = true,
}: MultiItemFieldMenuItemProps<T>) => {
const [isHovered, setIsHovered] = useState(false);
const { isDropdownOpen, closeDropdown } = useDropdown(dropdownId);
Expand Down Expand Up @@ -74,7 +76,7 @@ export const MultiItemFieldMenuItem = <T,>({
clickableComponent={iconButton}
dropdownComponents={
<DropdownMenuItemsContainer>
{!isPrimary && (
{hasPrimaryButton && !isPrimary && (
<MenuItem
LeftIcon={IconBookmarkPlus}
text="Set as Primary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ export type FieldActorMetadata = {
fieldName: string;
};

export type FieldArrayMetadata = {
objectMetadataNameSingular?: string;
fieldName: string;
values: { label: string; value: string }[];
};

export type FieldMetadata =
| FieldBooleanMetadata
| FieldCurrencyMetadata
Expand All @@ -174,7 +180,8 @@ export type FieldMetadata =
| FieldTextMetadata
| FieldUuidMetadata
| FieldAddressMetadata
| FieldActorMetadata;
| FieldActorMetadata
| FieldArrayMetadata;

export type FieldTextValue = string;
export type FieldUUidValue = string;
Expand Down Expand Up @@ -230,3 +237,5 @@ export type FieldActorValue = {
workspaceMemberId?: string;
name: string;
};

export type FieldArrayValue = string[];
Loading

0 comments on commit 10d86ee

Please sign in to comment.