feat: add Relation field form (#2572)
* feat: add useCreateOneRelationMetadata and useRelationMetadata Closes #2423 * feat: add Relation field form Closes #2003 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1014,6 +1014,26 @@ export type UserV2Edge = {
|
||||
node: UserV2;
|
||||
};
|
||||
|
||||
export type WorkspaceV2 = {
|
||||
__typename?: 'workspaceV2';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
deletedAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
displayName?: Maybe<Scalars['String']['output']>;
|
||||
domainName?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['ID']['output'];
|
||||
inviteHash?: Maybe<Scalars['String']['output']>;
|
||||
logo?: Maybe<Scalars['String']['output']>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type WorkspaceV2Edge = {
|
||||
__typename?: 'workspaceV2Edge';
|
||||
/** Cursor for this node. */
|
||||
cursor: Scalars['ConnectionCursor']['output'];
|
||||
/** The node containing the workspaceV2 */
|
||||
node: WorkspaceV2;
|
||||
};
|
||||
|
||||
export type CreateOneObjectMetadataItemMutationVariables = Exact<{
|
||||
input: CreateOneObjectInput;
|
||||
}>;
|
||||
|
||||
@ -1462,6 +1462,7 @@ export type Mutation = {
|
||||
updateOnePerson?: Maybe<Person>;
|
||||
updateOnePipelineProgress?: Maybe<PipelineProgress>;
|
||||
updateOnePipelineStage?: Maybe<PipelineStage>;
|
||||
updateOneWorkspaceV2: WorkspaceV2;
|
||||
updateUser: User;
|
||||
updateWorkspace: Workspace;
|
||||
uploadAttachment: Scalars['String'];
|
||||
@ -1668,6 +1669,11 @@ export type MutationUpdateOnePipelineStageArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateOneWorkspaceV2Args = {
|
||||
input: UpdateOneWorkspaceV2Input;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateUserArgs = {
|
||||
data: UserUpdateInput;
|
||||
where: UserWhereUniqueInput;
|
||||
@ -2486,6 +2492,8 @@ export type Query = {
|
||||
objects: ObjectConnection;
|
||||
relation: Relation;
|
||||
relations: RelationConnection;
|
||||
workspaceV2: WorkspaceV2;
|
||||
workspaceV2s: WorkspaceV2Connection;
|
||||
};
|
||||
|
||||
|
||||
@ -2613,6 +2621,18 @@ export type QueryFindWorkspaceFromInviteHashArgs = {
|
||||
inviteHash: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryWorkspaceV2Args = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryWorkspaceV2sArgs = {
|
||||
filter?: WorkspaceV2Filter;
|
||||
paging?: CursorPaging;
|
||||
sorting?: Array<WorkspaceV2Sort>;
|
||||
};
|
||||
|
||||
export enum QueryMode {
|
||||
Default = 'default',
|
||||
Insensitive = 'insensitive'
|
||||
@ -2647,6 +2667,18 @@ export enum RelationMetadataType {
|
||||
OneToOne = 'ONE_TO_ONE'
|
||||
}
|
||||
|
||||
/** Sort Directions */
|
||||
export enum SortDirection {
|
||||
Asc = 'ASC',
|
||||
Desc = 'DESC'
|
||||
}
|
||||
|
||||
/** Sort Nulls Options */
|
||||
export enum SortNulls {
|
||||
NullsFirst = 'NULLS_FIRST',
|
||||
NullsLast = 'NULLS_LAST'
|
||||
}
|
||||
|
||||
export enum SortOrder {
|
||||
Asc = 'asc',
|
||||
Desc = 'desc'
|
||||
@ -2694,6 +2726,20 @@ export type Telemetry = {
|
||||
enabled: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type UpdateOneWorkspaceV2Input = {
|
||||
/** The id of the record to update */
|
||||
id: Scalars['ID'];
|
||||
/** The update to apply. */
|
||||
update: UpdateWorkspaceInput;
|
||||
};
|
||||
|
||||
export type UpdateWorkspaceInput = {
|
||||
displayName: Scalars['String'];
|
||||
domainName: Scalars['String'];
|
||||
inviteHash: Scalars['String'];
|
||||
logo: Scalars['String'];
|
||||
};
|
||||
|
||||
export type User = {
|
||||
__typename?: 'User';
|
||||
assignedActivities?: Maybe<Array<Activity>>;
|
||||
@ -3150,6 +3196,16 @@ export type WorkspaceUpdateInput = {
|
||||
workspaceMember?: InputMaybe<WorkspaceMemberUpdateManyWithoutWorkspaceNestedInput>;
|
||||
};
|
||||
|
||||
export type WorkspaceV2Connection = {
|
||||
__typename?: 'WorkspaceV2Connection';
|
||||
/** Array of edges. */
|
||||
edges: Array<WorkspaceV2Edge>;
|
||||
/** Paging information */
|
||||
pageInfo: PageInfo;
|
||||
/** Fetch total count of records */
|
||||
totalCount: Scalars['Int'];
|
||||
};
|
||||
|
||||
export type Field = {
|
||||
__typename?: 'field';
|
||||
createdAt: Scalars['DateTime'];
|
||||
@ -3287,6 +3343,42 @@ export type UserV2Edge = {
|
||||
node: UserV2;
|
||||
};
|
||||
|
||||
export type WorkspaceV2 = {
|
||||
__typename?: 'workspaceV2';
|
||||
createdAt: Scalars['DateTime'];
|
||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||
displayName?: Maybe<Scalars['String']>;
|
||||
domainName?: Maybe<Scalars['String']>;
|
||||
id: Scalars['ID'];
|
||||
inviteHash?: Maybe<Scalars['String']>;
|
||||
logo?: Maybe<Scalars['String']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type WorkspaceV2Edge = {
|
||||
__typename?: 'workspaceV2Edge';
|
||||
/** Cursor for this node. */
|
||||
cursor: Scalars['ConnectionCursor'];
|
||||
/** The node containing the workspaceV2 */
|
||||
node: WorkspaceV2;
|
||||
};
|
||||
|
||||
export type WorkspaceV2Filter = {
|
||||
and?: InputMaybe<Array<WorkspaceV2Filter>>;
|
||||
id?: InputMaybe<IdFilterComparison>;
|
||||
or?: InputMaybe<Array<WorkspaceV2Filter>>;
|
||||
};
|
||||
|
||||
export type WorkspaceV2Sort = {
|
||||
direction: SortDirection;
|
||||
field: WorkspaceV2SortFields;
|
||||
nulls?: InputMaybe<SortNulls>;
|
||||
};
|
||||
|
||||
export enum WorkspaceV2SortFields {
|
||||
Id = 'id'
|
||||
}
|
||||
|
||||
export type CreateEventMutationVariables = Exact<{
|
||||
type: Scalars['String'];
|
||||
data: Scalars['JSON'];
|
||||
|
||||
@ -35,6 +35,11 @@ export const useObjectMetadataItemForSettings = () => {
|
||||
(objectMetadataItem) => objectMetadataItem.id === id,
|
||||
);
|
||||
|
||||
const findObjectMetadataItemByNamePlural = (namePlural: string) =>
|
||||
objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.namePlural === namePlural,
|
||||
);
|
||||
|
||||
const { createOneObjectMetadataItem } =
|
||||
useCreateOneObjectRecordMetadataItem();
|
||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||
@ -88,6 +93,8 @@ export const useObjectMetadataItemForSettings = () => {
|
||||
eraseObjectMetadataItem,
|
||||
findActiveObjectMetadataItemBySlug,
|
||||
findObjectMetadataItemById,
|
||||
findObjectMetadataItemByNamePlural,
|
||||
loading,
|
||||
objectMetadataItems,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,39 +1,51 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { FieldDefinitionRelationType } from '@/ui/object/field/types/FieldDefinition';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const parseFieldRelationType = (
|
||||
field: FieldMetadataItem | undefined,
|
||||
): FieldDefinitionRelationType | undefined => {
|
||||
if (field && field.type === 'RELATION') {
|
||||
if (
|
||||
isDefined(field.fromRelationMetadata) &&
|
||||
field.fromRelationMetadata.relationType === 'ONE_TO_MANY'
|
||||
) {
|
||||
return 'FROM_NAMY_OBJECTS';
|
||||
} else if (
|
||||
isDefined(field.toRelationMetadata) &&
|
||||
field.toRelationMetadata.relationType === 'ONE_TO_MANY'
|
||||
) {
|
||||
return 'TO_ONE_OBJECT';
|
||||
} else if (
|
||||
isDefined(field.fromRelationMetadata) &&
|
||||
field.fromRelationMetadata.relationType === 'MANY_TO_MANY'
|
||||
) {
|
||||
return 'FROM_NAMY_OBJECTS';
|
||||
} else if (
|
||||
isDefined(field.toRelationMetadata) &&
|
||||
field.toRelationMetadata.relationType === 'MANY_TO_MANY'
|
||||
) {
|
||||
return 'TO_MANY_OBJECTS';
|
||||
}
|
||||
if (!field || field.type !== FieldMetadataType.Relation) return;
|
||||
|
||||
throw new Error(
|
||||
`Cannot determine field relation type for field : ${JSON.stringify(
|
||||
field,
|
||||
)}.`,
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
const config: Record<
|
||||
RelationMetadataType,
|
||||
{ from: FieldDefinitionRelationType; to: FieldDefinitionRelationType }
|
||||
> = {
|
||||
[RelationMetadataType.ManyToMany]: {
|
||||
from: 'FROM_MANY_OBJECTS',
|
||||
to: 'TO_MANY_OBJECTS',
|
||||
},
|
||||
[RelationMetadataType.OneToMany]: {
|
||||
from: 'FROM_MANY_OBJECTS',
|
||||
to: 'TO_ONE_OBJECT',
|
||||
},
|
||||
[RelationMetadataType.OneToOne]: {
|
||||
from: 'FROM_ONE_OBJECT',
|
||||
to: 'TO_ONE_OBJECT',
|
||||
},
|
||||
};
|
||||
|
||||
if (
|
||||
isDefined(field.fromRelationMetadata) &&
|
||||
field.fromRelationMetadata.relationType in config
|
||||
) {
|
||||
return config[field.fromRelationMetadata.relationType].from;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(field.toRelationMetadata) &&
|
||||
field.toRelationMetadata.relationType in config
|
||||
) {
|
||||
return config[field.toRelationMetadata.relationType].to;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Cannot determine field relation type for field : ${JSON.stringify(
|
||||
field,
|
||||
)}.`,
|
||||
);
|
||||
};
|
||||
|
||||
12
front/src/modules/settings/data-model/assets/OneToMany.svg
Normal file
12
front/src/modules/settings/data-model/assets/OneToMany.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 46" width="58" height="46" fill="none" preserveAspectRatio="xMidYMid meet">
|
||||
<rect width="9" height="9" x=".5" y="18.5" stroke="#EBEBEB" rx="2.5" />
|
||||
<rect width="4" height="4" x="3" y="21" fill="#D6D6D6" rx="1" />
|
||||
<rect width="9" height="9" x="48.5" y=".5" stroke="#EBEBEB" rx="2.5" />
|
||||
<rect width="4" height="4" x="51" y="3" fill="#D6D6D6" rx="1" />
|
||||
<rect width="9" height="9" x="48.5" y="18.5" stroke="#EBEBEB" rx="2.5" />
|
||||
<rect width="4" height="4" x="51" y="21" fill="#D6D6D6" rx="1" />
|
||||
<rect width="9" height="9" x="48.5" y="36.5" stroke="#EBEBEB" rx="2.5" />
|
||||
<rect width="4" height="4" x="51" y="39" fill="#D6D6D6" rx="1" />
|
||||
<path fill="#D6D6D6" d="M5.113 22.5h48v1h-48v-1Z" />
|
||||
<path stroke="#D6D6D6" d="M52.884 41H45.06a7.544 7.544 0 0 1-7.56-7.561V12.56A7.544 7.544 0 0 1 45.06 5h7.793" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 870 B |
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="58" height="46" fill="none" viewBox="0 0 58 46">
|
||||
<rect width="9" height="9" x=".5" y="18.5" stroke="#EBEBEB" rx="2.5"/>
|
||||
<rect width="4" height="4" x="3" y="21" fill="#D6D6D6" rx="1"/>
|
||||
<rect width="9" height="9" x="48.5" y="18.5" stroke="#EBEBEB" rx="2.5"/>
|
||||
<rect width="4" height="4" x="51" y="21" fill="#D6D6D6" rx="1"/>
|
||||
<path fill="#D6D6D6" d="M5.113 22.5h48v1h-48v-1Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 437 B |
@ -1,30 +1,25 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { parseFieldType } from '@/object-metadata/utils/parseFieldType';
|
||||
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
|
||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||
import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon';
|
||||
import { FieldDisplay } from '@/ui/object/field/components/FieldDisplay';
|
||||
import { FieldContext } from '@/ui/object/field/contexts/FieldContext';
|
||||
import { BooleanFieldInput } from '@/ui/object/field/meta-types/input/components/BooleanFieldInput';
|
||||
import { entityFieldsFamilySelector } from '@/ui/object/field/states/selectors/entityFieldsFamilySelector';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
import { Field } from '~/generated/graphql';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { SettingsObjectFieldPreviewValueEffect } from '../components/SettingsObjectFieldPreviewValueEffect';
|
||||
import { dataTypes } from '../constants/dataTypes';
|
||||
import { useFieldPreview } from '../hooks/useFieldPreview';
|
||||
import { useRelationFieldPreview } from '../hooks/useRelationFieldPreview';
|
||||
|
||||
export type SettingsObjectFieldPreviewProps = {
|
||||
fieldIconKey?: string | null;
|
||||
fieldLabel: string;
|
||||
fieldName?: string;
|
||||
fieldType: FieldMetadataType;
|
||||
isObjectCustom: boolean;
|
||||
objectIconKey?: string | null;
|
||||
objectLabelPlural: string;
|
||||
objectNamePlural: string;
|
||||
className?: string;
|
||||
fieldMetadata: Pick<Field, 'icon' | 'label' | 'type'> & { id?: string };
|
||||
objectMetadataId: string;
|
||||
relationObjectMetadataId?: string;
|
||||
shrink?: boolean;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -52,7 +47,7 @@ const StyledObjectName = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledFieldPreview = styled.div`
|
||||
const StyledFieldPreview = styled.div<{ shrink?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
@ -61,7 +56,8 @@ const StyledFieldPreview = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
overflow: hidden;
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
padding: 0
|
||||
${({ shrink, theme }) => (shrink ? theme.spacing(1) : theme.spacing(2))};
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
@ -73,41 +69,41 @@ const StyledFieldLabel = styled.div`
|
||||
`;
|
||||
|
||||
export const SettingsObjectFieldPreview = ({
|
||||
fieldIconKey,
|
||||
fieldLabel,
|
||||
fieldName,
|
||||
fieldType,
|
||||
isObjectCustom,
|
||||
objectIconKey,
|
||||
objectLabelPlural,
|
||||
objectNamePlural,
|
||||
className,
|
||||
fieldMetadata,
|
||||
objectMetadataId,
|
||||
relationObjectMetadataId,
|
||||
shrink,
|
||||
}: SettingsObjectFieldPreviewProps) => {
|
||||
const theme = useTheme();
|
||||
const { Icon: ObjectIcon } = useLazyLoadIcon(objectIconKey ?? '');
|
||||
const { Icon: FieldIcon } = useLazyLoadIcon(fieldIconKey ?? '');
|
||||
|
||||
const { objects } = useFindManyObjectRecords({
|
||||
objectNamePlural,
|
||||
skip: !fieldName,
|
||||
const {
|
||||
entityId,
|
||||
FieldIcon,
|
||||
fieldName,
|
||||
hasValue,
|
||||
ObjectIcon,
|
||||
objectMetadataItem,
|
||||
value,
|
||||
} = useFieldPreview({
|
||||
fieldMetadata,
|
||||
objectMetadataId,
|
||||
});
|
||||
|
||||
const [fieldValue, setFieldValue] = useRecoilState(
|
||||
entityFieldsFamilySelector({
|
||||
entityId: objects[0]?.id ?? objectNamePlural,
|
||||
fieldName: fieldName || 'new-field',
|
||||
}),
|
||||
);
|
||||
const { defaultValue: relationDefaultValue, entityChipDisplayMapper } =
|
||||
useRelationFieldPreview({
|
||||
relationObjectMetadataId,
|
||||
skipDefaultValue:
|
||||
fieldMetadata.type !== FieldMetadataType.Relation || hasValue,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(
|
||||
fieldName && assertNotNull(objects[0]?.[fieldName])
|
||||
? objects[0][fieldName]
|
||||
: dataTypes[fieldType].defaultValue,
|
||||
);
|
||||
}, [fieldName, fieldType, fieldValue, objects, setFieldValue]);
|
||||
const defaultValue =
|
||||
fieldMetadata.type === FieldMetadataType.Relation
|
||||
? relationDefaultValue
|
||||
: dataTypes[fieldMetadata.type].defaultValue;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledContainer className={className}>
|
||||
<StyledObjectSummary>
|
||||
<StyledObjectName>
|
||||
{!!ObjectIcon && (
|
||||
@ -116,15 +112,20 @@ export const SettingsObjectFieldPreview = ({
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
)}
|
||||
{objectLabelPlural}
|
||||
{objectMetadataItem?.labelPlural}
|
||||
</StyledObjectName>
|
||||
{isObjectCustom ? (
|
||||
{objectMetadataItem?.isCustom ? (
|
||||
<Tag color="orange" text="Custom" />
|
||||
) : (
|
||||
<Tag color="blue" text="Standard" />
|
||||
)}
|
||||
</StyledObjectSummary>
|
||||
<StyledFieldPreview>
|
||||
<SettingsObjectFieldPreviewValueEffect
|
||||
entityId={entityId}
|
||||
fieldName={fieldName}
|
||||
value={value ?? defaultValue}
|
||||
/>
|
||||
<StyledFieldPreview shrink={shrink}>
|
||||
<StyledFieldLabel>
|
||||
{!!FieldIcon && (
|
||||
<FieldIcon
|
||||
@ -132,22 +133,26 @@ export const SettingsObjectFieldPreview = ({
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
)}
|
||||
{fieldLabel}:
|
||||
{fieldMetadata.label}:
|
||||
</StyledFieldLabel>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId: objects[0]?.id ?? objectNamePlural,
|
||||
entityId,
|
||||
fieldDefinition: {
|
||||
type: parseFieldType(fieldType as FieldMetadataType),
|
||||
type: parseFieldType(fieldMetadata.type),
|
||||
Icon: FieldIcon,
|
||||
fieldMetadataId: '',
|
||||
label: fieldLabel,
|
||||
metadata: { fieldName: fieldName || 'new-field' },
|
||||
fieldMetadataId: fieldMetadata.id || '',
|
||||
label: fieldMetadata.label,
|
||||
metadata: { fieldName },
|
||||
entityChipDisplayMapper:
|
||||
fieldMetadata.type === FieldMetadataType.Relation
|
||||
? entityChipDisplayMapper
|
||||
: undefined,
|
||||
},
|
||||
hotkeyScope: 'field-preview',
|
||||
}}
|
||||
>
|
||||
{fieldType === 'BOOLEAN' ? (
|
||||
{fieldMetadata.type === FieldMetadataType.Boolean ? (
|
||||
<BooleanFieldInput readonly />
|
||||
) : (
|
||||
<FieldDisplay />
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { entityFieldsFamilySelector } from '@/ui/object/field/states/selectors/entityFieldsFamilySelector';
|
||||
|
||||
type SettingsObjectFieldPreviewValueEffectProps = {
|
||||
entityId: string;
|
||||
fieldName: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
export const SettingsObjectFieldPreviewValueEffect = ({
|
||||
entityId,
|
||||
fieldName,
|
||||
value,
|
||||
}: SettingsObjectFieldPreviewValueEffectProps) => {
|
||||
const [, setFieldValue] = useRecoilState(
|
||||
entityFieldsFamilySelector({
|
||||
entityId,
|
||||
fieldName,
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFieldValue(value);
|
||||
}, [value, setFieldValue]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -0,0 +1,123 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
|
||||
import { Field } from '~/generated-metadata/graphql';
|
||||
|
||||
import { relationTypes } from '../constants/relationTypes';
|
||||
import { RelationType } from '../types/RelationType';
|
||||
|
||||
export type SettingsObjectFieldRelationFormValues = Partial<{
|
||||
field: Partial<Pick<Field, 'icon' | 'label'>>;
|
||||
objectMetadataId: string;
|
||||
type: RelationType;
|
||||
}>;
|
||||
|
||||
type SettingsObjectFieldRelationFormProps = {
|
||||
disableRelationEdition?: boolean;
|
||||
onChange: (values: SettingsObjectFieldRelationFormValues) => void;
|
||||
values?: SettingsObjectFieldRelationFormValues;
|
||||
};
|
||||
|
||||
const StyledSelectsContainer = styled.div`
|
||||
display: grid;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledInputsLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: block;
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SettingsObjectFieldRelationForm = ({
|
||||
disableRelationEdition,
|
||||
onChange,
|
||||
values,
|
||||
}: SettingsObjectFieldRelationFormProps) => {
|
||||
const { icons } = useLazyLoadIcons();
|
||||
const { objectMetadataItems, findObjectMetadataItemById } =
|
||||
useObjectMetadataItemForSettings();
|
||||
|
||||
const selectedObjectMetadataItem =
|
||||
(values?.objectMetadataId
|
||||
? findObjectMetadataItemById(values.objectMetadataId)
|
||||
: undefined) || objectMetadataItems[0];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StyledSelectsContainer>
|
||||
<Select
|
||||
label="Relation type"
|
||||
dropdownScopeId="relation-type-select"
|
||||
disabled={disableRelationEdition}
|
||||
value={values?.type}
|
||||
options={Object.entries(relationTypes).map(
|
||||
([value, { label, Icon }]) => ({
|
||||
label,
|
||||
value: value as RelationType,
|
||||
Icon,
|
||||
}),
|
||||
)}
|
||||
onChange={(value) => onChange({ type: value })}
|
||||
/>
|
||||
<Select
|
||||
label="Object destination"
|
||||
dropdownScopeId="object-destination-select"
|
||||
disabled={disableRelationEdition}
|
||||
value={values?.objectMetadataId}
|
||||
options={objectMetadataItems.map((objectMetadataItem) => ({
|
||||
label: objectMetadataItem.labelPlural,
|
||||
value: objectMetadataItem.id,
|
||||
Icon: objectMetadataItem.icon
|
||||
? icons[objectMetadataItem.icon]
|
||||
: undefined,
|
||||
}))}
|
||||
onChange={(value) => onChange({ objectMetadataId: value })}
|
||||
/>
|
||||
</StyledSelectsContainer>
|
||||
<StyledInputsLabel>
|
||||
Field on {selectedObjectMetadataItem?.labelPlural}
|
||||
</StyledInputsLabel>
|
||||
<StyledInputsContainer>
|
||||
<IconPicker
|
||||
dropdownScopeId="field-destination-icon-picker"
|
||||
selectedIconKey={values?.field?.icon || undefined}
|
||||
onChange={(value) =>
|
||||
onChange({
|
||||
field: { ...values?.field, icon: value.iconKey },
|
||||
})
|
||||
}
|
||||
variant="primary"
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Field name"
|
||||
value={values?.field?.label || ''}
|
||||
onChange={(value) => {
|
||||
if (!value || validateMetadataLabel(value)) {
|
||||
onChange({
|
||||
field: { ...values?.field, label: value },
|
||||
});
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -27,6 +27,11 @@ const StyledTitle = styled.h3`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledPreviewContent = styled.div`
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
const StyledFormContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
@ -46,7 +51,7 @@ export const SettingsObjectFieldTypeCard = ({
|
||||
<div className={className}>
|
||||
<StyledPreviewContainer>
|
||||
<StyledTitle>Preview</StyledTitle>
|
||||
{preview}
|
||||
<StyledPreviewContent>{preview}</StyledPreviewContent>
|
||||
</StyledPreviewContainer>
|
||||
{!!form && <StyledFormContainer>{form}</StyledFormContainer>}
|
||||
</div>
|
||||
|
||||
@ -3,82 +3,142 @@ import styled from '@emotion/styled';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { dataTypes } from '../constants/dataTypes';
|
||||
import { relationTypes } from '../constants/relationTypes';
|
||||
|
||||
import {
|
||||
SettingsObjectFieldPreview,
|
||||
SettingsObjectFieldPreviewProps,
|
||||
} from './SettingsObjectFieldPreview';
|
||||
import {
|
||||
SettingsObjectFieldRelationForm,
|
||||
SettingsObjectFieldRelationFormValues,
|
||||
} from './SettingsObjectFieldRelationForm';
|
||||
import { SettingsObjectFieldTypeCard } from './SettingsObjectFieldTypeCard';
|
||||
|
||||
export type SettingsObjectFieldTypeSelectSectionFormValues = Partial<{
|
||||
type: FieldMetadataType;
|
||||
relation: SettingsObjectFieldRelationFormValues;
|
||||
}>;
|
||||
|
||||
type SettingsObjectFieldTypeSelectSectionProps = {
|
||||
disabled?: boolean;
|
||||
onChange?: (value: FieldMetadataType) => void;
|
||||
} & Pick<
|
||||
SettingsObjectFieldPreviewProps,
|
||||
| 'fieldIconKey'
|
||||
| 'fieldLabel'
|
||||
| 'fieldName'
|
||||
| 'fieldType'
|
||||
| 'isObjectCustom'
|
||||
| 'objectIconKey'
|
||||
| 'objectLabelPlural'
|
||||
| 'objectNamePlural'
|
||||
>;
|
||||
fieldMetadata: Pick<Field, 'icon' | 'label'> & { id?: string };
|
||||
relationFieldMetadataId?: string;
|
||||
onChange: (values: SettingsObjectFieldTypeSelectSectionFormValues) => void;
|
||||
values?: SettingsObjectFieldTypeSelectSectionFormValues;
|
||||
} & Pick<SettingsObjectFieldPreviewProps, 'objectMetadataId'>;
|
||||
|
||||
const StyledSettingsObjectFieldTypeCard = styled(SettingsObjectFieldTypeCard)`
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
// TODO: remove "enum" and "relation" types for now, add them back when the backend is ready.
|
||||
const { ENUM: _ENUM, RELATION: _RELATION, ...allowedDataTypes } = dataTypes;
|
||||
const StyledSettingsObjectFieldPreview = styled(SettingsObjectFieldPreview)`
|
||||
display: grid;
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
|
||||
const StyledRelationImage = styled.img<{ flip?: boolean }>`
|
||||
transform: ${({ flip }) => (flip ? 'scaleX(-1)' : 'none')};
|
||||
width: 54px;
|
||||
`;
|
||||
|
||||
export const SettingsObjectFieldTypeSelectSection = ({
|
||||
disabled,
|
||||
fieldIconKey,
|
||||
fieldLabel,
|
||||
fieldName,
|
||||
fieldType,
|
||||
isObjectCustom,
|
||||
objectIconKey,
|
||||
objectLabelPlural,
|
||||
objectNamePlural,
|
||||
fieldMetadata,
|
||||
relationFieldMetadataId,
|
||||
objectMetadataId,
|
||||
onChange,
|
||||
}: SettingsObjectFieldTypeSelectSectionProps) => (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Type and values"
|
||||
description="The field's type and values."
|
||||
/>
|
||||
<Select
|
||||
disabled={disabled}
|
||||
dropdownScopeId="object-field-type-select"
|
||||
value={fieldType}
|
||||
onChange={onChange}
|
||||
options={Object.entries(allowedDataTypes).map(([key, dataType]) => ({
|
||||
value: key as FieldMetadataType,
|
||||
...dataType,
|
||||
}))}
|
||||
/>
|
||||
{['BOOLEAN', 'DATE', 'MONEY', 'NUMBER', 'TEXT', 'URL'].includes(
|
||||
fieldType,
|
||||
) && (
|
||||
<StyledSettingsObjectFieldTypeCard
|
||||
preview={
|
||||
<SettingsObjectFieldPreview
|
||||
fieldIconKey={fieldIconKey}
|
||||
fieldLabel={fieldLabel}
|
||||
fieldName={fieldName}
|
||||
fieldType={fieldType}
|
||||
isObjectCustom={isObjectCustom}
|
||||
objectIconKey={objectIconKey}
|
||||
objectLabelPlural={objectLabelPlural}
|
||||
objectNamePlural={objectNamePlural}
|
||||
/>
|
||||
}
|
||||
values,
|
||||
}: SettingsObjectFieldTypeSelectSectionProps) => {
|
||||
const relationFormConfig = values?.relation;
|
||||
|
||||
const allowedFieldTypes = Object.entries(dataTypes).filter(
|
||||
([key]) => key !== FieldMetadataType.Relation,
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Type and values"
|
||||
description="The field's type and values."
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
<Select
|
||||
disabled={!!fieldMetadata.id}
|
||||
dropdownScopeId="object-field-type-select"
|
||||
value={values?.type}
|
||||
onChange={(value) => onChange({ type: value })}
|
||||
options={allowedFieldTypes.map(([key, dataType]) => ({
|
||||
value: key as FieldMetadataType,
|
||||
...dataType,
|
||||
}))}
|
||||
/>
|
||||
{!!values?.type &&
|
||||
[
|
||||
FieldMetadataType.Boolean,
|
||||
FieldMetadataType.Currency,
|
||||
FieldMetadataType.Date,
|
||||
FieldMetadataType.Link,
|
||||
FieldMetadataType.Number,
|
||||
FieldMetadataType.Relation,
|
||||
FieldMetadataType.Text,
|
||||
].includes(values.type) && (
|
||||
<StyledSettingsObjectFieldTypeCard
|
||||
preview={
|
||||
<>
|
||||
<StyledSettingsObjectFieldPreview
|
||||
fieldMetadata={{
|
||||
...fieldMetadata,
|
||||
type: values.type,
|
||||
}}
|
||||
shrink={values.type === FieldMetadataType.Relation}
|
||||
objectMetadataId={objectMetadataId}
|
||||
relationObjectMetadataId={
|
||||
relationFormConfig?.objectMetadataId
|
||||
}
|
||||
/>
|
||||
{values.type === FieldMetadataType.Relation &&
|
||||
!!relationFormConfig?.type &&
|
||||
!!relationFormConfig.objectMetadataId && (
|
||||
<>
|
||||
<StyledRelationImage
|
||||
src={relationTypes[relationFormConfig.type].imageSrc}
|
||||
flip={
|
||||
relationTypes[relationFormConfig.type].isImageFlipped
|
||||
}
|
||||
alt={relationTypes[relationFormConfig.type].label}
|
||||
/>
|
||||
<StyledSettingsObjectFieldPreview
|
||||
fieldMetadata={{
|
||||
...relationFormConfig.field,
|
||||
label:
|
||||
relationFormConfig.field?.label || 'Field name',
|
||||
type: FieldMetadataType.Relation,
|
||||
id: relationFieldMetadataId,
|
||||
}}
|
||||
shrink
|
||||
objectMetadataId={relationFormConfig.objectMetadataId}
|
||||
relationObjectMetadataId={objectMetadataId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
form={
|
||||
values.type === FieldMetadataType.Relation && (
|
||||
<SettingsObjectFieldRelationForm
|
||||
disableRelationEdition={!!relationFieldMetadataId}
|
||||
values={relationFormConfig}
|
||||
onChange={(nextValues) =>
|
||||
onChange({
|
||||
relation: { ...relationFormConfig, ...nextValues },
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,91 +0,0 @@
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { SettingsObjectFieldPreview } from '../SettingsObjectFieldPreview';
|
||||
|
||||
const meta: Meta<typeof SettingsObjectFieldPreview> = {
|
||||
title: 'Modules/Settings/DataModel/SettingsObjectFieldPreview',
|
||||
component: SettingsObjectFieldPreview,
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
fieldIconKey: 'IconNotes',
|
||||
fieldLabel: 'Description',
|
||||
fieldType: FieldMetadataType.Text,
|
||||
isObjectCustom: false,
|
||||
objectIconKey: 'IconBuildingSkyscraper',
|
||||
objectLabelPlural: 'Companies',
|
||||
objectNamePlural: 'companies',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsObjectFieldPreview>;
|
||||
|
||||
export const Text: Story = {};
|
||||
|
||||
export const Boolean: Story = {
|
||||
args: {
|
||||
fieldIconKey: 'IconHeadphones',
|
||||
fieldLabel: 'Priority Support',
|
||||
fieldType: FieldMetadataType.Boolean,
|
||||
},
|
||||
};
|
||||
|
||||
export const Currency: Story = {
|
||||
args: {
|
||||
fieldIconKey: 'IconCurrencyDollar',
|
||||
fieldLabel: 'Amount',
|
||||
fieldType: FieldMetadataType.Currency,
|
||||
},
|
||||
};
|
||||
|
||||
export const Date: Story = {
|
||||
args: {
|
||||
fieldIconKey: 'IconCalendarEvent',
|
||||
fieldLabel: 'Registration Date',
|
||||
fieldType: FieldMetadataType.Date,
|
||||
},
|
||||
};
|
||||
|
||||
export const Link: Story = {
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<MemoryRouter>
|
||||
<Story />
|
||||
</MemoryRouter>
|
||||
),
|
||||
],
|
||||
args: {
|
||||
fieldIconKey: 'IconWorldWww',
|
||||
fieldLabel: 'Website',
|
||||
fieldType: FieldMetadataType.Link,
|
||||
},
|
||||
};
|
||||
|
||||
export const Number: Story = {
|
||||
args: {
|
||||
fieldIconKey: 'IconUsers',
|
||||
fieldLabel: 'Employees',
|
||||
fieldType: FieldMetadataType.Number,
|
||||
},
|
||||
};
|
||||
|
||||
export const Select: Story = {
|
||||
args: {
|
||||
fieldIconKey: 'IconBuildingFactory2',
|
||||
fieldLabel: 'Industry',
|
||||
fieldType: FieldMetadataType.Enum,
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomObject: Story = {
|
||||
args: {
|
||||
isObjectCustom: true,
|
||||
objectIconKey: 'IconApps',
|
||||
objectLabelPlural: 'Workspaces',
|
||||
objectNamePlural: 'workspaces',
|
||||
},
|
||||
};
|
||||
@ -1,36 +0,0 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { SettingsObjectFieldPreview } from '../SettingsObjectFieldPreview';
|
||||
import { SettingsObjectFieldTypeCard } from '../SettingsObjectFieldTypeCard';
|
||||
|
||||
const meta: Meta<typeof SettingsObjectFieldTypeCard> = {
|
||||
title: 'Modules/Settings/DataModel/SettingsObjectFieldTypeCard',
|
||||
component: SettingsObjectFieldTypeCard,
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
preview: (
|
||||
<SettingsObjectFieldPreview
|
||||
fieldIconKey="IconNotes"
|
||||
fieldLabel="Description"
|
||||
fieldType={FieldMetadataType.Text}
|
||||
isObjectCustom={false}
|
||||
objectIconKey="IconUser"
|
||||
objectLabelPlural="People"
|
||||
objectNamePlural="people"
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsObjectFieldTypeCard>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithForm: Story = {
|
||||
args: { form: <TextInput label="Lorem ipsum" placeholder="Lorem ipsum" /> },
|
||||
};
|
||||
@ -1,7 +1,6 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { SettingsObjectFieldTypeSelectSection } from '../SettingsObjectFieldTypeSelectSection';
|
||||
@ -10,16 +9,7 @@ const meta: Meta<typeof SettingsObjectFieldTypeSelectSection> = {
|
||||
title: 'Modules/Settings/DataModel/SettingsObjectFieldTypeSelectSection',
|
||||
component: SettingsObjectFieldTypeSelectSection,
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
fieldType: FieldMetadataType.Number,
|
||||
fieldIconKey: 'IconUsers',
|
||||
fieldLabel: 'Employees',
|
||||
fieldName: 'employees',
|
||||
isObjectCustom: false,
|
||||
objectIconKey: 'IconUser',
|
||||
objectLabelPlural: 'People',
|
||||
objectNamePlural: 'people',
|
||||
},
|
||||
args: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@ -28,7 +18,7 @@ type Story = StoryObj<typeof SettingsObjectFieldTypeSelectSection>;
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: { disabled: true },
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const WithOpenSelect: Story = {
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
IconMail,
|
||||
IconNumbers,
|
||||
IconPhone,
|
||||
IconPlug,
|
||||
IconRelationManyToMany,
|
||||
IconTag,
|
||||
IconTextSize,
|
||||
IconUser,
|
||||
@ -61,9 +61,12 @@ export const dataTypes: Record<
|
||||
[FieldMetadataType.Currency]: {
|
||||
label: 'Currency',
|
||||
Icon: IconCoins,
|
||||
defaultValue: { amount: 2000, currency: CurrencyCode.Usd },
|
||||
defaultValue: { amountMicros: 2000000000, currencyCode: CurrencyCode.Usd },
|
||||
},
|
||||
[FieldMetadataType.Relation]: {
|
||||
label: 'Relation',
|
||||
Icon: IconRelationManyToMany,
|
||||
},
|
||||
[FieldMetadataType.Relation]: { label: 'Relation', Icon: IconPlug },
|
||||
[FieldMetadataType.Email]: { label: 'Email', Icon: IconMail },
|
||||
[FieldMetadataType.Phone]: { label: 'Phone', Icon: IconPhone },
|
||||
[FieldMetadataType.Probability]: {
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import { IconRelationOneToMany, IconRelationOneToOne } from '@/ui/display/icon';
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { RelationMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import OneToManySvg from '../assets/OneToMany.svg';
|
||||
import OneToOneSvg from '../assets/OneToOne.svg';
|
||||
import { RelationType } from '../types/RelationType';
|
||||
|
||||
export const relationTypes: Record<
|
||||
RelationType,
|
||||
{
|
||||
label: string;
|
||||
Icon: IconComponent;
|
||||
imageSrc: string;
|
||||
isImageFlipped?: boolean;
|
||||
}
|
||||
> = {
|
||||
[RelationMetadataType.OneToMany]: {
|
||||
label: 'Has many',
|
||||
Icon: IconRelationOneToMany,
|
||||
imageSrc: OneToManySvg,
|
||||
},
|
||||
[RelationMetadataType.OneToOne]: {
|
||||
label: 'Has one',
|
||||
Icon: IconRelationOneToOne,
|
||||
imageSrc: OneToOneSvg,
|
||||
},
|
||||
MANY_TO_ONE: {
|
||||
label: 'Belongs to one',
|
||||
Icon: IconRelationOneToMany,
|
||||
imageSrc: OneToManySvg,
|
||||
isImageFlipped: true,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,142 @@
|
||||
import { useState } from 'react';
|
||||
import { DeepPartial } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { SettingsObjectFieldTypeSelectSectionFormValues } from '../components/SettingsObjectFieldTypeSelectSection';
|
||||
|
||||
type FormValues = {
|
||||
description?: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
type: FieldMetadataType;
|
||||
relation: SettingsObjectFieldTypeSelectSectionFormValues['relation'];
|
||||
};
|
||||
|
||||
const defaultValues: FormValues = {
|
||||
icon: 'IconUsers',
|
||||
label: '',
|
||||
type: FieldMetadataType.Text,
|
||||
relation: {
|
||||
type: RelationMetadataType.OneToMany,
|
||||
},
|
||||
};
|
||||
|
||||
const fieldSchema = z.object({
|
||||
description: z.string().optional(),
|
||||
icon: z.string().startsWith('Icon'),
|
||||
label: z.string().min(1),
|
||||
});
|
||||
|
||||
const relationSchema = fieldSchema.merge(
|
||||
z.object({
|
||||
type: z.literal(FieldMetadataType.Relation),
|
||||
relation: z.object({
|
||||
field: fieldSchema,
|
||||
objectMetadataId: z.string().uuid(),
|
||||
type: z.enum([
|
||||
RelationMetadataType.OneToMany,
|
||||
RelationMetadataType.OneToOne,
|
||||
'MANY_TO_ONE',
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const { Relation: _, ...otherFieldTypes } = FieldMetadataType;
|
||||
|
||||
const otherFieldTypesSchema = fieldSchema.merge(
|
||||
z.object({
|
||||
type: z.enum(
|
||||
Object.values(otherFieldTypes) as [
|
||||
Exclude<FieldMetadataType, FieldMetadataType.Relation>,
|
||||
...Exclude<FieldMetadataType, FieldMetadataType.Relation>[],
|
||||
],
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const schema = z.discriminatedUnion('type', [
|
||||
relationSchema,
|
||||
otherFieldTypesSchema,
|
||||
]);
|
||||
|
||||
export const useFieldMetadataForm = () => {
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [initialFormValues, setInitialFormValues] =
|
||||
useState<FormValues>(defaultValues);
|
||||
const [formValues, setFormValues] = useState<FormValues>(defaultValues);
|
||||
const [hasFieldFormChanged, setHasFieldFormChanged] = useState(false);
|
||||
const [hasRelationFormChanged, setHasRelationFormChanged] = useState(false);
|
||||
const [validationResult, setValidationResult] = useState(
|
||||
schema.safeParse(formValues),
|
||||
);
|
||||
|
||||
const mergePartialValues = (
|
||||
previousValues: FormValues,
|
||||
nextValues: DeepPartial<FormValues>,
|
||||
) => ({
|
||||
...previousValues,
|
||||
...nextValues,
|
||||
relation: {
|
||||
...previousValues.relation,
|
||||
...nextValues.relation,
|
||||
field: {
|
||||
...previousValues.relation?.field,
|
||||
...nextValues.relation?.field,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const initForm = (lazyInitialFormValues: DeepPartial<FormValues>) => {
|
||||
if (isInitialized) return;
|
||||
|
||||
const mergedFormValues = mergePartialValues(
|
||||
initialFormValues,
|
||||
lazyInitialFormValues,
|
||||
);
|
||||
|
||||
setInitialFormValues(mergedFormValues);
|
||||
setFormValues(mergedFormValues);
|
||||
setValidationResult(schema.safeParse(mergedFormValues));
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
const handleFormChange = (values: DeepPartial<FormValues>) => {
|
||||
const nextFormValues = mergePartialValues(formValues, values);
|
||||
|
||||
setFormValues(nextFormValues);
|
||||
setValidationResult(schema.safeParse(nextFormValues));
|
||||
|
||||
const { relation: initialRelationFormValues, ...initialFieldFormValues } =
|
||||
initialFormValues;
|
||||
const { relation: nextRelationFormValues, ...nextFieldFormValues } =
|
||||
nextFormValues;
|
||||
|
||||
setHasFieldFormChanged(
|
||||
!isDeeplyEqual(initialFieldFormValues, nextFieldFormValues),
|
||||
);
|
||||
setHasRelationFormChanged(
|
||||
nextFieldFormValues.type === FieldMetadataType.Relation &&
|
||||
!isDeeplyEqual(initialRelationFormValues, nextRelationFormValues),
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
formValues,
|
||||
handleFormChange,
|
||||
hasFieldFormChanged,
|
||||
hasFormChanged: hasFieldFormChanged || hasRelationFormChanged,
|
||||
hasRelationFormChanged,
|
||||
initForm,
|
||||
isValid: validationResult.success,
|
||||
validatedFormValues: validationResult.success
|
||||
? validationResult.data
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
|
||||
import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon';
|
||||
import { Field } from '~/generated-metadata/graphql';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
|
||||
export const useFieldPreview = ({
|
||||
fieldMetadata,
|
||||
objectMetadataId,
|
||||
}: {
|
||||
fieldMetadata: Partial<Pick<Field, 'icon' | 'id' | 'type'>>;
|
||||
objectMetadataId: string;
|
||||
}) => {
|
||||
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
|
||||
const objectMetadataItem = findObjectMetadataItemById(objectMetadataId);
|
||||
|
||||
const { objects } = useFindManyObjectRecords({
|
||||
objectNamePlural: objectMetadataItem?.namePlural,
|
||||
skip: !objectMetadataItem || !fieldMetadata.id,
|
||||
});
|
||||
|
||||
const { Icon: ObjectIcon } = useLazyLoadIcon(objectMetadataItem?.icon ?? '');
|
||||
const { Icon: FieldIcon } = useLazyLoadIcon(fieldMetadata.icon ?? '');
|
||||
|
||||
const [firstRecord] = objects;
|
||||
const fieldName = fieldMetadata.id
|
||||
? objectMetadataItem?.fields.find(({ id }) => id === fieldMetadata.id)?.name
|
||||
: undefined;
|
||||
const value = fieldName ? firstRecord?.[fieldName] : undefined;
|
||||
|
||||
return {
|
||||
entityId: firstRecord?.id || `${objectMetadataId}-no-records`,
|
||||
FieldIcon,
|
||||
fieldName: fieldName || `${fieldMetadata.type}-new-field`,
|
||||
hasValue: assertNotNull(value),
|
||||
ObjectIcon,
|
||||
objectMetadataItem,
|
||||
value,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
|
||||
|
||||
export const useRelationFieldPreview = ({
|
||||
relationObjectMetadataId,
|
||||
skipDefaultValue,
|
||||
}: {
|
||||
relationObjectMetadataId?: string;
|
||||
skipDefaultValue: boolean;
|
||||
}) => {
|
||||
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
|
||||
|
||||
const relationObjectMetadataItem = relationObjectMetadataId
|
||||
? findObjectMetadataItemById(relationObjectMetadataId)
|
||||
: undefined;
|
||||
|
||||
const { objects: relationObjects } = useFindManyObjectRecords({
|
||||
objectNamePlural: relationObjectMetadataItem?.namePlural,
|
||||
skip: skipDefaultValue || !relationObjectMetadataItem,
|
||||
});
|
||||
|
||||
return {
|
||||
defaultValue: relationObjects?.[0],
|
||||
entityChipDisplayMapper: (fieldValue?: { id: string }) => ({
|
||||
name: fieldValue?.id || relationObjectMetadataItem?.labelSingular || '',
|
||||
avatarType: 'squared' as const,
|
||||
}),
|
||||
};
|
||||
};
|
||||
@ -16,7 +16,7 @@ const StyledDataType = styled.div<{ value: FieldMetadataType }>`
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
${({ theme, value }) =>
|
||||
value === 'RELATION'
|
||||
value === FieldMetadataType.Relation
|
||||
? css`
|
||||
border-color: ${theme.color.purple20};
|
||||
color: ${theme.color.purple};
|
||||
|
||||
@ -6,7 +6,6 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { dataTypes } from '../../constants/dataTypes';
|
||||
|
||||
@ -31,9 +30,6 @@ const StyledIconTableCell = styled(TableCell)`
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
// TODO: remove "relation" type for now, add it back when the backend is ready.
|
||||
const { RELATION: _, ...dataTypesWithoutRelation } = dataTypes;
|
||||
|
||||
export const SettingsObjectFieldItemTableRow = ({
|
||||
ActionIcon,
|
||||
fieldItem,
|
||||
@ -42,13 +38,11 @@ export const SettingsObjectFieldItemTableRow = ({
|
||||
const { Icon } = useLazyLoadIcon(fieldItem.icon ?? '');
|
||||
|
||||
// TODO: parse with zod and merge types with FieldType (create a subset of FieldType for example)
|
||||
const fieldDataTypeIsSupported = Object.keys(
|
||||
dataTypesWithoutRelation,
|
||||
).includes(fieldItem.type);
|
||||
const fieldDataTypeIsSupported = Object.keys(dataTypes).includes(
|
||||
fieldItem.type,
|
||||
);
|
||||
|
||||
if (!fieldDataTypeIsSupported) {
|
||||
return null;
|
||||
}
|
||||
if (!fieldDataTypeIsSupported) return null;
|
||||
|
||||
return (
|
||||
<StyledObjectFieldTableRow>
|
||||
@ -58,9 +52,7 @@ export const SettingsObjectFieldItemTableRow = ({
|
||||
</StyledNameTableCell>
|
||||
<TableCell>{fieldItem.isCustom ? 'Custom' : 'Standard'}</TableCell>
|
||||
<TableCell>
|
||||
<SettingsObjectFieldDataType
|
||||
value={fieldItem.type as FieldMetadataType}
|
||||
/>
|
||||
<SettingsObjectFieldDataType value={fieldItem.type} />
|
||||
</TableCell>
|
||||
<StyledIconTableCell>{ActionIcon}</StyledIconTableCell>
|
||||
</StyledObjectFieldTableRow>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { MouseEvent, ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { OverflowingTextWithTooltip } from '../../tooltip/OverflowingTextWithTooltip';
|
||||
@ -28,9 +28,10 @@ type ChipProps = {
|
||||
maxWidth?: string;
|
||||
variant?: ChipVariant;
|
||||
accent?: ChipAccent;
|
||||
leftComponent?: React.ReactNode;
|
||||
rightComponent?: React.ReactNode;
|
||||
leftComponent?: ReactNode;
|
||||
rightComponent?: ReactNode;
|
||||
className?: string;
|
||||
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div<Partial<ChipProps>>`
|
||||
@ -125,6 +126,7 @@ export const Chip = ({
|
||||
accent = ChipAccent.TextPrimary,
|
||||
maxWidth,
|
||||
className,
|
||||
onClick,
|
||||
}: ChipProps) => (
|
||||
<StyledContainer
|
||||
data-testid="chip"
|
||||
@ -135,6 +137,7 @@ export const Chip = ({
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
maxWidth={maxWidth}
|
||||
onClick={onClick}
|
||||
>
|
||||
{leftComponent}
|
||||
<StyledLabel>
|
||||
|
||||
@ -45,31 +45,31 @@ export const EntityChip = ({
|
||||
};
|
||||
|
||||
return isNonEmptyString(name) ? (
|
||||
<div onClick={handleLinkClick}>
|
||||
<Chip
|
||||
label={name}
|
||||
variant={
|
||||
linkToEntity
|
||||
? variant === EntityChipVariant.Regular
|
||||
? ChipVariant.Highlighted
|
||||
: ChipVariant.Regular
|
||||
: ChipVariant.Transparent
|
||||
}
|
||||
leftComponent={
|
||||
LeftIcon ? (
|
||||
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
) : (
|
||||
<Avatar
|
||||
avatarUrl={pictureUrl}
|
||||
colorId={entityId}
|
||||
placeholder={name}
|
||||
size="sm"
|
||||
type={avatarType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Chip
|
||||
label={name}
|
||||
variant={
|
||||
linkToEntity
|
||||
? variant === EntityChipVariant.Regular
|
||||
? ChipVariant.Highlighted
|
||||
: ChipVariant.Regular
|
||||
: ChipVariant.Transparent
|
||||
}
|
||||
leftComponent={
|
||||
LeftIcon ? (
|
||||
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
) : (
|
||||
<Avatar
|
||||
avatarUrl={pictureUrl}
|
||||
colorId={entityId}
|
||||
placeholder={name}
|
||||
size="sm"
|
||||
type={avatarType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
clickable={!!linkToEntity}
|
||||
onClick={handleLinkClick}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
@ -69,6 +69,9 @@ export {
|
||||
IconPlug,
|
||||
IconPlus,
|
||||
IconProgressCheck,
|
||||
IconRelationManyToMany,
|
||||
IconRelationOneToMany,
|
||||
IconRelationOneToOne,
|
||||
IconRepeat,
|
||||
IconRobot,
|
||||
IconSearch,
|
||||
|
||||
@ -19,6 +19,7 @@ import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
|
||||
|
||||
type IconPickerProps = {
|
||||
disabled?: boolean;
|
||||
dropdownScopeId?: string;
|
||||
onChange: (params: { iconKey: string; Icon: IconComponent }) => void;
|
||||
selectedIconKey?: string;
|
||||
onClickOutside?: () => void;
|
||||
@ -44,6 +45,7 @@ const convertIconKeyToLabel = (iconKey: string) =>
|
||||
|
||||
export const IconPicker = ({
|
||||
disabled,
|
||||
dropdownScopeId = 'icon-picker',
|
||||
onChange,
|
||||
selectedIconKey,
|
||||
onClickOutside,
|
||||
@ -53,7 +55,7 @@ export const IconPicker = ({
|
||||
}: IconPickerProps) => {
|
||||
const [searchString, setSearchString] = useState('');
|
||||
|
||||
const { closeDropdown } = useDropdown({ dropdownScopeId: 'icon-picker' });
|
||||
const { closeDropdown } = useDropdown({ dropdownScopeId });
|
||||
|
||||
const { icons, isLoadingIcons: isLoading } = useLazyLoadIcons();
|
||||
|
||||
@ -75,7 +77,7 @@ export const IconPicker = ({
|
||||
}, [icons, searchString, selectedIconKey]);
|
||||
|
||||
return (
|
||||
<DropdownScope dropdownScopeId="icon-picker">
|
||||
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
||||
<Dropdown
|
||||
dropdownHotkeyScope={{ scope: IconPickerHotkeyScope.IconPicker }}
|
||||
clickableComponent={
|
||||
|
||||
@ -12,14 +12,16 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
||||
|
||||
export type SelectProps<Value extends string | number | null> = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
dropdownScopeId: string;
|
||||
label?: string;
|
||||
onChange?: (value: Value) => void;
|
||||
options: { value: Value; label: string; Icon?: IconComponent }[];
|
||||
value?: Value;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div<{ disabled?: boolean }>`
|
||||
const StyledControlContainer = styled.div<{ disabled?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
@ -34,7 +36,16 @@ const StyledContainer = styled.div<{ disabled?: boolean }>`
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.div`
|
||||
const StyledLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: block;
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const StyledControlLabel = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
@ -46,8 +57,10 @@ const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>`
|
||||
`;
|
||||
|
||||
export const Select = <Value extends string | number | null>({
|
||||
className,
|
||||
disabled,
|
||||
dropdownScopeId,
|
||||
label,
|
||||
onChange,
|
||||
options,
|
||||
value,
|
||||
@ -59,46 +72,49 @@ export const Select = <Value extends string | number | null>({
|
||||
const { closeDropdown } = useDropdown({ dropdownScopeId });
|
||||
|
||||
const selectControl = (
|
||||
<StyledContainer disabled={disabled}>
|
||||
<StyledLabel>
|
||||
{!!selectedOption.Icon && (
|
||||
<StyledControlContainer disabled={disabled}>
|
||||
<StyledControlLabel>
|
||||
{!!selectedOption?.Icon && (
|
||||
<selectedOption.Icon
|
||||
color={disabled ? theme.font.color.light : theme.font.color.primary}
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
)}
|
||||
{selectedOption.label}
|
||||
</StyledLabel>
|
||||
{selectedOption?.label}
|
||||
</StyledControlLabel>
|
||||
<StyledIconChevronDown disabled={disabled} size={theme.icon.size.md} />
|
||||
</StyledContainer>
|
||||
</StyledControlContainer>
|
||||
);
|
||||
|
||||
return disabled ? (
|
||||
selectControl
|
||||
) : (
|
||||
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
||||
<Dropdown
|
||||
dropdownMenuWidth={176}
|
||||
dropdownPlacement="bottom-start"
|
||||
clickableComponent={selectControl}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
{options.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
LeftIcon={option.Icon}
|
||||
text={option.label}
|
||||
onClick={() => {
|
||||
onChange?.(option.value);
|
||||
closeDropdown();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
|
||||
/>
|
||||
<div className={className}>
|
||||
{!!label && <StyledLabel>{label}</StyledLabel>}
|
||||
<Dropdown
|
||||
dropdownMenuWidth={176}
|
||||
dropdownPlacement="bottom-start"
|
||||
clickableComponent={selectControl}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
{options.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
LeftIcon={option.Icon}
|
||||
text={option.label}
|
||||
onClick={() => {
|
||||
onChange?.(option.value);
|
||||
closeDropdown();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
|
||||
/>
|
||||
</div>
|
||||
</DropdownScope>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,8 +5,6 @@ const StyledPanel = styled.div`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
|
||||
@ -7,9 +7,10 @@ export const getEntityChipFromFieldMetadata = (
|
||||
fieldDefinition: FieldDefinition<FieldRelationMetadata>,
|
||||
fieldValue: any,
|
||||
) => {
|
||||
const { entityChipDisplayMapper } = fieldDefinition;
|
||||
const { fieldName } = fieldDefinition.metadata;
|
||||
|
||||
const chipValue: Pick<
|
||||
const defaultChipValue: Pick<
|
||||
EntityChipProps,
|
||||
'name' | 'pictureUrl' | 'avatarType' | 'entityId'
|
||||
> = {
|
||||
@ -19,15 +20,23 @@ export const getEntityChipFromFieldMetadata = (
|
||||
entityId: fieldValue?.id,
|
||||
};
|
||||
|
||||
// TODO: use every
|
||||
if (fieldName === 'accountOwner' && fieldValue) {
|
||||
chipValue.name = fieldValue.name.firstName + ' ' + fieldValue.name.lastName;
|
||||
} else if (fieldName === 'company' && fieldValue) {
|
||||
chipValue.name = fieldValue.name;
|
||||
chipValue.pictureUrl = getLogoUrlFromDomainName(fieldValue.domainName);
|
||||
} else if (fieldName === 'person' && fieldValue) {
|
||||
chipValue.name = fieldValue.name.firstName + ' ' + fieldValue.name.lastName;
|
||||
if (['accountOwner', 'person'].includes(fieldName) && fieldValue) {
|
||||
return {
|
||||
...defaultChipValue,
|
||||
name: `${fieldValue.firstName} ${fieldValue.lastName}`,
|
||||
};
|
||||
}
|
||||
|
||||
return chipValue;
|
||||
if (fieldName === 'company' && fieldValue) {
|
||||
return {
|
||||
...defaultChipValue,
|
||||
name: fieldValue.name,
|
||||
pictureUrl: getLogoUrlFromDomainName(fieldValue.domainName),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultChipValue,
|
||||
...entityChipDisplayMapper?.(fieldValue),
|
||||
};
|
||||
};
|
||||
|
||||
@ -5,9 +5,10 @@ import { FieldMetadata } from './FieldMetadata';
|
||||
import { FieldType } from './FieldType';
|
||||
|
||||
export type FieldDefinitionRelationType =
|
||||
| 'TO_ONE_OBJECT'
|
||||
| 'FROM_NAMY_OBJECTS'
|
||||
| 'TO_MANY_OBJECTS';
|
||||
| 'FROM_MANY_OBJECTS'
|
||||
| 'FROM_ONE_OBJECT'
|
||||
| 'TO_MANY_OBJECTS'
|
||||
| 'TO_ONE_OBJECT';
|
||||
|
||||
export type FieldDefinition<T extends FieldMetadata> = {
|
||||
fieldMetadataId: string;
|
||||
|
||||
@ -1,8 +1,20 @@
|
||||
import { isNull, isString } from '@sniptt/guards';
|
||||
|
||||
import { formatToHumanReadableDate } from '~/utils';
|
||||
|
||||
import { FieldDateValue } from '../FieldMetadata';
|
||||
|
||||
// TODO: add zod
|
||||
export const isFieldDateValue = (
|
||||
fieldValue: unknown,
|
||||
): fieldValue is FieldDateValue => isNull(fieldValue) || isString(fieldValue);
|
||||
): fieldValue is FieldDateValue => {
|
||||
try {
|
||||
if (isNull(fieldValue)) return true;
|
||||
if (isString(fieldValue)) {
|
||||
formatToHumanReadableDate(fieldValue);
|
||||
return true;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useRelationMetadata } from '@/object-metadata/hooks/useRelationMetadata';
|
||||
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
|
||||
import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection';
|
||||
import { useFieldMetadataForm } from '@/settings/data-model/hooks/useFieldMetadataForm';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { IconArchive, IconSettings } from '@/ui/display/icon';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
@ -34,13 +36,22 @@ export const SettingsObjectFieldEdit = () => {
|
||||
metadataField.isActive && getFieldSlug(metadataField) === fieldSlug,
|
||||
);
|
||||
|
||||
const [formValues, setFormValues] = useState<
|
||||
Partial<{
|
||||
icon: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}>
|
||||
>({});
|
||||
const {
|
||||
relationFieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
relationType,
|
||||
} = useRelationMetadata({ fieldMetadataItem: activeMetadataField });
|
||||
|
||||
const {
|
||||
formValues,
|
||||
handleFormChange,
|
||||
hasFieldFormChanged,
|
||||
hasFormChanged,
|
||||
hasRelationFormChanged,
|
||||
initForm,
|
||||
isValid,
|
||||
validatedFormValues,
|
||||
} = useFieldMetadataForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
@ -50,36 +61,59 @@ export const SettingsObjectFieldEdit = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Object.keys(formValues).length) {
|
||||
setFormValues({
|
||||
icon: activeMetadataField.icon ?? undefined,
|
||||
label: activeMetadataField.label,
|
||||
description: activeMetadataField.description ?? undefined,
|
||||
});
|
||||
}
|
||||
initForm({
|
||||
icon: activeMetadataField.icon ?? undefined,
|
||||
label: activeMetadataField.label,
|
||||
description: activeMetadataField.description ?? undefined,
|
||||
type: activeMetadataField.type,
|
||||
relation: {
|
||||
field: {
|
||||
icon: relationFieldMetadataItem?.icon,
|
||||
label: relationFieldMetadataItem?.label,
|
||||
},
|
||||
objectMetadataId: relationObjectMetadataItem?.id,
|
||||
type: relationType,
|
||||
},
|
||||
});
|
||||
}, [
|
||||
activeMetadataField,
|
||||
activeObjectMetadataItem,
|
||||
formValues,
|
||||
initForm,
|
||||
loading,
|
||||
navigate,
|
||||
relationFieldMetadataItem?.icon,
|
||||
relationFieldMetadataItem?.label,
|
||||
relationObjectMetadataItem?.id,
|
||||
relationType,
|
||||
]);
|
||||
|
||||
if (!activeObjectMetadataItem || !activeMetadataField) return null;
|
||||
|
||||
const areRequiredFieldsFilled = !!formValues.label;
|
||||
|
||||
const hasChanges =
|
||||
formValues.description !== activeMetadataField.description ||
|
||||
formValues.icon !== activeMetadataField.icon ||
|
||||
formValues.label !== activeMetadataField.label;
|
||||
|
||||
const canSave = areRequiredFieldsFilled && hasChanges;
|
||||
const canSave = isValid && hasFormChanged;
|
||||
|
||||
const handleSave = async () => {
|
||||
const editedField = { ...activeMetadataField, ...formValues };
|
||||
if (!validatedFormValues) return;
|
||||
|
||||
await editMetadataField(editedField);
|
||||
if (
|
||||
validatedFormValues.type === FieldMetadataType.Relation &&
|
||||
relationFieldMetadataItem?.id &&
|
||||
hasRelationFormChanged
|
||||
) {
|
||||
await editMetadataField({
|
||||
icon: validatedFormValues.relation.field.icon,
|
||||
id: relationFieldMetadataItem.id,
|
||||
label: validatedFormValues.relation.field.label,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasFieldFormChanged) {
|
||||
await editMetadataField({
|
||||
description: validatedFormValues.description,
|
||||
icon: validatedFormValues.icon,
|
||||
id: activeMetadataField.id,
|
||||
label: validatedFormValues.label,
|
||||
});
|
||||
}
|
||||
|
||||
navigate(`/settings/objects/${objectSlug}`);
|
||||
};
|
||||
@ -116,23 +150,21 @@ export const SettingsObjectFieldEdit = () => {
|
||||
name={formValues.label}
|
||||
description={formValues.description}
|
||||
iconKey={formValues.icon}
|
||||
onChange={(values) =>
|
||||
setFormValues((previousFormValues) => ({
|
||||
...previousFormValues,
|
||||
...values,
|
||||
}))
|
||||
}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
<SettingsObjectFieldTypeSelectSection
|
||||
disabled
|
||||
fieldIconKey={formValues.icon}
|
||||
fieldLabel={formValues.label || 'Employees'}
|
||||
fieldName={activeMetadataField.name}
|
||||
fieldType={activeMetadataField.type as FieldMetadataType}
|
||||
isObjectCustom={activeObjectMetadataItem.isCustom}
|
||||
objectIconKey={activeObjectMetadataItem.icon}
|
||||
objectLabelPlural={activeObjectMetadataItem.labelPlural}
|
||||
objectNamePlural={activeObjectMetadataItem.namePlural}
|
||||
fieldMetadata={{
|
||||
icon: formValues.icon,
|
||||
label: formValues.label || 'Employees',
|
||||
id: activeMetadataField.id,
|
||||
}}
|
||||
objectMetadataId={activeObjectMetadataItem.id}
|
||||
onChange={handleFormChange}
|
||||
relationFieldMetadataId={relationFieldMetadataItem?.id}
|
||||
values={{
|
||||
type: formValues.type,
|
||||
relation: formValues.relation,
|
||||
}}
|
||||
/>
|
||||
<Section>
|
||||
<H2Title title="Danger zone" description="Disable this field" />
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { useCreateOneRelationMetadata } from '@/object-metadata/hooks/useCreateOneRelationMetadata';
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { useCreateOneObjectRecord } from '@/object-record/hooks/useCreateOneObjectRecord';
|
||||
@ -11,6 +12,7 @@ import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderCon
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
|
||||
import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection';
|
||||
import { useFieldMetadataForm } from '@/settings/data-model/hooks/useFieldMetadataForm';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { IconSettings } from '@/ui/display/icon';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
@ -23,26 +25,48 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { objectSlug = '' } = useParams();
|
||||
|
||||
const { findActiveObjectMetadataItemBySlug, loading } =
|
||||
useObjectMetadataItemForSettings();
|
||||
const {
|
||||
findActiveObjectMetadataItemBySlug,
|
||||
findObjectMetadataItemById,
|
||||
findObjectMetadataItemByNamePlural,
|
||||
loading,
|
||||
} = useObjectMetadataItemForSettings();
|
||||
|
||||
const activeObjectMetadataItem =
|
||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||
const { createMetadataField } = useFieldMetadataItem();
|
||||
|
||||
const {
|
||||
formValues,
|
||||
handleFormChange,
|
||||
initForm,
|
||||
isValid: canSave,
|
||||
validatedFormValues,
|
||||
} = useFieldMetadataForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
if (!activeObjectMetadataItem) navigate(AppPath.NotFound);
|
||||
}, [activeObjectMetadataItem, loading, navigate]);
|
||||
if (!activeObjectMetadataItem) {
|
||||
navigate(AppPath.NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
const [formValues, setFormValues] = useState<{
|
||||
description?: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
type: FieldMetadataType;
|
||||
}>({ icon: 'IconUsers', label: '', type: FieldMetadataType.Number });
|
||||
initForm({
|
||||
relation: {
|
||||
field: { icon: activeObjectMetadataItem.icon },
|
||||
objectMetadataId: findObjectMetadataItemByNamePlural('peopleV2')?.id,
|
||||
},
|
||||
});
|
||||
}, [
|
||||
activeObjectMetadataItem,
|
||||
findObjectMetadataItemByNamePlural,
|
||||
initForm,
|
||||
loading,
|
||||
navigate,
|
||||
]);
|
||||
|
||||
const [objectViews, setObjectViews] = useState<View[]>([]);
|
||||
const [relationObjectViews, setRelationObjectViews] = useState<View[]>([]);
|
||||
|
||||
const { createOneObject: createOneViewField } = useCreateOneObjectRecord({
|
||||
objectNameSingular: 'viewFieldV2',
|
||||
@ -57,32 +81,100 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
onCompleted: async (data: PaginatedObjectTypeResults<View>) => {
|
||||
const views = data.edges;
|
||||
|
||||
if (!views) {
|
||||
return;
|
||||
}
|
||||
if (!views) return;
|
||||
|
||||
setObjectViews(data.edges.map(({ node }) => node));
|
||||
},
|
||||
});
|
||||
|
||||
useFindManyObjectRecords({
|
||||
objectNamePlural: 'viewsV2',
|
||||
skip: !formValues.relation?.objectMetadataId,
|
||||
filter: {
|
||||
type: { eq: ViewType.Table },
|
||||
objectMetadataId: { eq: formValues.relation?.objectMetadataId },
|
||||
},
|
||||
onCompleted: async (data: PaginatedObjectTypeResults<View>) => {
|
||||
const views = data.edges;
|
||||
|
||||
if (!views) return;
|
||||
|
||||
setRelationObjectViews(data.edges.map(({ node }) => node));
|
||||
},
|
||||
});
|
||||
|
||||
const { createOneRelationMetadata } = useCreateOneRelationMetadata();
|
||||
|
||||
if (!activeObjectMetadataItem) return null;
|
||||
|
||||
const canSave = !!formValues.label;
|
||||
|
||||
const handleSave = async () => {
|
||||
const createdField = await createMetadataField({
|
||||
...formValues,
|
||||
objectMetadataId: activeObjectMetadataItem.id,
|
||||
});
|
||||
objectViews.forEach(async (view) => {
|
||||
await createOneViewField?.({
|
||||
view: view.id,
|
||||
fieldMetadataId: createdField.data?.createOneField.id,
|
||||
position: activeObjectMetadataItem.fields.length,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
if (!validatedFormValues) return;
|
||||
|
||||
if (validatedFormValues.type === FieldMetadataType.Relation) {
|
||||
const createdRelation = await createOneRelationMetadata({
|
||||
relationType: validatedFormValues.relation.type,
|
||||
field: {
|
||||
description: validatedFormValues.description,
|
||||
icon: validatedFormValues.icon,
|
||||
label: validatedFormValues.label,
|
||||
},
|
||||
objectMetadataId: activeObjectMetadataItem.id,
|
||||
connect: {
|
||||
field: {
|
||||
icon: validatedFormValues.relation.field.icon,
|
||||
label: validatedFormValues.relation.field.label,
|
||||
},
|
||||
objectMetadataId: validatedFormValues.relation.objectMetadataId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const relationObjectMetadataItem = findObjectMetadataItemById(
|
||||
validatedFormValues.relation.objectMetadataId,
|
||||
);
|
||||
|
||||
objectViews.forEach(async (view) => {
|
||||
await createOneViewField?.({
|
||||
view: view.id,
|
||||
fieldMetadataId:
|
||||
validatedFormValues.relation.type === 'MANY_TO_ONE'
|
||||
? createdRelation.data?.createOneRelation.toFieldMetadataId
|
||||
: createdRelation.data?.createOneRelation.fromFieldMetadataId,
|
||||
position: activeObjectMetadataItem.fields.length,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
});
|
||||
});
|
||||
relationObjectViews.forEach(async (view) => {
|
||||
await createOneViewField?.({
|
||||
view: view.id,
|
||||
fieldMetadataId:
|
||||
validatedFormValues.relation.type === 'MANY_TO_ONE'
|
||||
? createdRelation.data?.createOneRelation.fromFieldMetadataId
|
||||
: createdRelation.data?.createOneRelation.toFieldMetadataId,
|
||||
position: relationObjectMetadataItem?.fields.length,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const createdField = await createMetadataField({
|
||||
description: validatedFormValues.description,
|
||||
icon: validatedFormValues.icon,
|
||||
label: validatedFormValues.label,
|
||||
objectMetadataId: activeObjectMetadataItem.id,
|
||||
type: validatedFormValues.type,
|
||||
});
|
||||
objectViews.forEach(async (view) => {
|
||||
await createOneViewField?.({
|
||||
view: view.id,
|
||||
fieldMetadataId: createdField.data?.createOneField.id,
|
||||
position: activeObjectMetadataItem.fields.length,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
navigate(`/settings/objects/${objectSlug}`);
|
||||
};
|
||||
|
||||
@ -110,24 +202,19 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
iconKey={formValues.icon}
|
||||
name={formValues.label}
|
||||
description={formValues.description}
|
||||
onChange={(values) =>
|
||||
setFormValues((previousValues) => ({
|
||||
...previousValues,
|
||||
...values,
|
||||
}))
|
||||
}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
<SettingsObjectFieldTypeSelectSection
|
||||
fieldIconKey={formValues.icon}
|
||||
fieldLabel={formValues.label || 'Employees'}
|
||||
fieldType={formValues.type}
|
||||
isObjectCustom={activeObjectMetadataItem.isCustom}
|
||||
objectIconKey={activeObjectMetadataItem.icon}
|
||||
objectLabelPlural={activeObjectMetadataItem.labelPlural}
|
||||
objectNamePlural={activeObjectMetadataItem.namePlural}
|
||||
onChange={(type) =>
|
||||
setFormValues((previousValues) => ({ ...previousValues, type }))
|
||||
}
|
||||
fieldMetadata={{
|
||||
icon: formValues.icon,
|
||||
label: formValues.label || 'Employees',
|
||||
}}
|
||||
objectMetadataId={activeObjectMetadataItem.id}
|
||||
onChange={handleFormChange}
|
||||
values={{
|
||||
type: formValues.type,
|
||||
relation: formValues.relation,
|
||||
}}
|
||||
/>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
|
||||
@ -1,3 +1,187 @@
|
||||
export const mockedCompaniesMetadata = {
|
||||
node: {
|
||||
id: 'a3195559-cc20-4749-9565-572a2f506581',
|
||||
dataSourceId: '',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
labelSingular: 'Company',
|
||||
labelPlural: 'Companies',
|
||||
description: null,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fields: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '397eabc0-c5a1-4550-8e68-839c878a8d0e',
|
||||
type: 'TEXT',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'The company name.',
|
||||
placeholder: null,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isNullable: false,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '7ad234c7-f3b9-4efc-813c-43dc97070b07',
|
||||
type: 'URL',
|
||||
name: 'URL',
|
||||
label: 'URL',
|
||||
description:
|
||||
'The company website URL. We use this url to fetch the company icon.',
|
||||
placeholder: null,
|
||||
icon: 'IconLink',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'a578ffb2-13db-483c-ace7-5c30a13dff2d',
|
||||
type: 'RELATION',
|
||||
name: 'accountOwner',
|
||||
label: 'Account Owner',
|
||||
description:
|
||||
'Your team member responsible for managing the company account.',
|
||||
placeholder: null,
|
||||
icon: 'IconUserCircle',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'b7fd622d-7d8b-4f5a-b148-a7e9fd2c4660',
|
||||
type: 'NUMBER',
|
||||
name: 'employees',
|
||||
label: 'Employees',
|
||||
description: 'Number of employees in the company.',
|
||||
placeholder: null,
|
||||
icon: 'IconUsers',
|
||||
isCustom: true,
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '60ab27ed-a959-471e-b583-887387f7accd',
|
||||
type: 'URL',
|
||||
name: 'linkedin',
|
||||
label: 'Linkedin',
|
||||
description: null,
|
||||
placeholder: null,
|
||||
icon: 'IconBrandLinkedin',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '6daadb98-83ca-4c85-bca5-7792a7d958ad',
|
||||
type: 'BOOLEAN',
|
||||
name: 'prioritySupport',
|
||||
label: 'Priority Support',
|
||||
description: 'Whether the company has priority support.',
|
||||
placeholder: null,
|
||||
icon: 'IconHeadphones',
|
||||
isCustom: true,
|
||||
isActive: false,
|
||||
isNullable: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
endCursor: null,
|
||||
},
|
||||
totalCount: 6,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockedWorkspacesMetadata = {
|
||||
node: {
|
||||
id: 'c973efa3-436e-47ea-8dbc-983ed869c04d',
|
||||
dataSourceId: '',
|
||||
nameSingular: 'workspace',
|
||||
namePlural: 'workspaces',
|
||||
labelSingular: 'Workspace',
|
||||
labelPlural: 'Workspaces',
|
||||
description: null,
|
||||
icon: 'IconApps',
|
||||
isCustom: true,
|
||||
isActive: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fields: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'f955402c-9e8f-4b91-a82c-95f6de392b99',
|
||||
type: 'TEXT',
|
||||
name: 'slug',
|
||||
label: 'Slug',
|
||||
description: null,
|
||||
placeholder: null,
|
||||
icon: 'IconSlash',
|
||||
isCustom: true,
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
endCursor: null,
|
||||
},
|
||||
totalCount: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockedObjectMetadataItems = {
|
||||
edges: [
|
||||
{
|
||||
@ -198,188 +382,8 @@ export const mockedObjectMetadataItems = {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'a3195559-cc20-4749-9565-572a2f506581',
|
||||
dataSourceId: '',
|
||||
nameSingular: 'company',
|
||||
namePlural: 'companies',
|
||||
labelSingular: 'Company',
|
||||
labelPlural: 'Companies',
|
||||
description: null,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fields: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '397eabc0-c5a1-4550-8e68-839c878a8d0e',
|
||||
type: 'TEXT',
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'The company name.',
|
||||
placeholder: null,
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isNullable: false,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '7ad234c7-f3b9-4efc-813c-43dc97070b07',
|
||||
type: 'URL',
|
||||
name: 'URL',
|
||||
label: 'URL',
|
||||
description:
|
||||
'The company website URL. We use this url to fetch the company icon.',
|
||||
placeholder: null,
|
||||
icon: 'IconLink',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'a578ffb2-13db-483c-ace7-5c30a13dff2d',
|
||||
type: 'RELATION',
|
||||
name: 'accountOwner',
|
||||
label: 'Account Owner',
|
||||
description:
|
||||
'Your team member responsible for managing the company account.',
|
||||
placeholder: null,
|
||||
icon: 'IconUserCircle',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'b7fd622d-7d8b-4f5a-b148-a7e9fd2c4660',
|
||||
type: 'NUMBER',
|
||||
name: 'employees',
|
||||
label: 'Employees',
|
||||
description: 'Number of employees in the company.',
|
||||
placeholder: null,
|
||||
icon: 'IconUsers',
|
||||
isCustom: true,
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '60ab27ed-a959-471e-b583-887387f7accd',
|
||||
type: 'URL',
|
||||
name: 'linkedin',
|
||||
label: 'Linkedin',
|
||||
description: null,
|
||||
placeholder: null,
|
||||
icon: 'IconBrandLinkedin',
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '6daadb98-83ca-4c85-bca5-7792a7d958ad',
|
||||
type: 'BOOLEAN',
|
||||
name: 'prioritySupport',
|
||||
label: 'Priority Support',
|
||||
description: 'Whether the company has priority support.',
|
||||
placeholder: null,
|
||||
icon: 'IconHeadphones',
|
||||
isCustom: true,
|
||||
isActive: false,
|
||||
isNullable: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
endCursor: null,
|
||||
},
|
||||
totalCount: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'c973efa3-436e-47ea-8dbc-983ed869c04d',
|
||||
dataSourceId: '',
|
||||
nameSingular: 'workspace',
|
||||
namePlural: 'workspaces',
|
||||
labelSingular: 'Workspace',
|
||||
labelPlural: 'Workspaces',
|
||||
description: null,
|
||||
icon: 'IconApps',
|
||||
isCustom: true,
|
||||
isActive: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fields: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'f955402c-9e8f-4b91-a82c-95f6de392b99',
|
||||
type: 'TEXT',
|
||||
name: 'slug',
|
||||
label: 'Slug',
|
||||
description: null,
|
||||
placeholder: null,
|
||||
icon: 'IconSlash',
|
||||
isCustom: true,
|
||||
isActive: true,
|
||||
isNullable: true,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
endCursor: null,
|
||||
},
|
||||
totalCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
mockedCompaniesMetadata,
|
||||
mockedWorkspacesMetadata,
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
|
||||
@ -9,8 +9,8 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('workspaces')
|
||||
@ObjectType('workspace')
|
||||
@Entity('workspaceV2')
|
||||
@ObjectType('workspaceV2')
|
||||
export class Workspace {
|
||||
@IDField(() => ID)
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
|
||||
Reference in New Issue
Block a user