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;
|
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<{
|
export type CreateOneObjectMetadataItemMutationVariables = Exact<{
|
||||||
input: CreateOneObjectInput;
|
input: CreateOneObjectInput;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@ -1462,6 +1462,7 @@ export type Mutation = {
|
|||||||
updateOnePerson?: Maybe<Person>;
|
updateOnePerson?: Maybe<Person>;
|
||||||
updateOnePipelineProgress?: Maybe<PipelineProgress>;
|
updateOnePipelineProgress?: Maybe<PipelineProgress>;
|
||||||
updateOnePipelineStage?: Maybe<PipelineStage>;
|
updateOnePipelineStage?: Maybe<PipelineStage>;
|
||||||
|
updateOneWorkspaceV2: WorkspaceV2;
|
||||||
updateUser: User;
|
updateUser: User;
|
||||||
updateWorkspace: Workspace;
|
updateWorkspace: Workspace;
|
||||||
uploadAttachment: Scalars['String'];
|
uploadAttachment: Scalars['String'];
|
||||||
@ -1668,6 +1669,11 @@ export type MutationUpdateOnePipelineStageArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpdateOneWorkspaceV2Args = {
|
||||||
|
input: UpdateOneWorkspaceV2Input;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationUpdateUserArgs = {
|
export type MutationUpdateUserArgs = {
|
||||||
data: UserUpdateInput;
|
data: UserUpdateInput;
|
||||||
where: UserWhereUniqueInput;
|
where: UserWhereUniqueInput;
|
||||||
@ -2486,6 +2492,8 @@ export type Query = {
|
|||||||
objects: ObjectConnection;
|
objects: ObjectConnection;
|
||||||
relation: Relation;
|
relation: Relation;
|
||||||
relations: RelationConnection;
|
relations: RelationConnection;
|
||||||
|
workspaceV2: WorkspaceV2;
|
||||||
|
workspaceV2s: WorkspaceV2Connection;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -2613,6 +2621,18 @@ export type QueryFindWorkspaceFromInviteHashArgs = {
|
|||||||
inviteHash: Scalars['String'];
|
inviteHash: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryWorkspaceV2Args = {
|
||||||
|
id: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryWorkspaceV2sArgs = {
|
||||||
|
filter?: WorkspaceV2Filter;
|
||||||
|
paging?: CursorPaging;
|
||||||
|
sorting?: Array<WorkspaceV2Sort>;
|
||||||
|
};
|
||||||
|
|
||||||
export enum QueryMode {
|
export enum QueryMode {
|
||||||
Default = 'default',
|
Default = 'default',
|
||||||
Insensitive = 'insensitive'
|
Insensitive = 'insensitive'
|
||||||
@ -2647,6 +2667,18 @@ export enum RelationMetadataType {
|
|||||||
OneToOne = 'ONE_TO_ONE'
|
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 {
|
export enum SortOrder {
|
||||||
Asc = 'asc',
|
Asc = 'asc',
|
||||||
Desc = 'desc'
|
Desc = 'desc'
|
||||||
@ -2694,6 +2726,20 @@ export type Telemetry = {
|
|||||||
enabled: Scalars['Boolean'];
|
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 = {
|
export type User = {
|
||||||
__typename?: 'User';
|
__typename?: 'User';
|
||||||
assignedActivities?: Maybe<Array<Activity>>;
|
assignedActivities?: Maybe<Array<Activity>>;
|
||||||
@ -3150,6 +3196,16 @@ export type WorkspaceUpdateInput = {
|
|||||||
workspaceMember?: InputMaybe<WorkspaceMemberUpdateManyWithoutWorkspaceNestedInput>;
|
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 = {
|
export type Field = {
|
||||||
__typename?: 'field';
|
__typename?: 'field';
|
||||||
createdAt: Scalars['DateTime'];
|
createdAt: Scalars['DateTime'];
|
||||||
@ -3287,6 +3343,42 @@ export type UserV2Edge = {
|
|||||||
node: UserV2;
|
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<{
|
export type CreateEventMutationVariables = Exact<{
|
||||||
type: Scalars['String'];
|
type: Scalars['String'];
|
||||||
data: Scalars['JSON'];
|
data: Scalars['JSON'];
|
||||||
|
|||||||
@ -35,6 +35,11 @@ export const useObjectMetadataItemForSettings = () => {
|
|||||||
(objectMetadataItem) => objectMetadataItem.id === id,
|
(objectMetadataItem) => objectMetadataItem.id === id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const findObjectMetadataItemByNamePlural = (namePlural: string) =>
|
||||||
|
objectMetadataItems.find(
|
||||||
|
(objectMetadataItem) => objectMetadataItem.namePlural === namePlural,
|
||||||
|
);
|
||||||
|
|
||||||
const { createOneObjectMetadataItem } =
|
const { createOneObjectMetadataItem } =
|
||||||
useCreateOneObjectRecordMetadataItem();
|
useCreateOneObjectRecordMetadataItem();
|
||||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
@ -88,6 +93,8 @@ export const useObjectMetadataItemForSettings = () => {
|
|||||||
eraseObjectMetadataItem,
|
eraseObjectMetadataItem,
|
||||||
findActiveObjectMetadataItemBySlug,
|
findActiveObjectMetadataItemBySlug,
|
||||||
findObjectMetadataItemById,
|
findObjectMetadataItemById,
|
||||||
|
findObjectMetadataItemByNamePlural,
|
||||||
loading,
|
loading,
|
||||||
|
objectMetadataItems,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,39 +1,51 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { FieldDefinitionRelationType } from '@/ui/object/field/types/FieldDefinition';
|
import { FieldDefinitionRelationType } from '@/ui/object/field/types/FieldDefinition';
|
||||||
|
import {
|
||||||
|
FieldMetadataType,
|
||||||
|
RelationMetadataType,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const parseFieldRelationType = (
|
export const parseFieldRelationType = (
|
||||||
field: FieldMetadataItem | undefined,
|
field: FieldMetadataItem | undefined,
|
||||||
): FieldDefinitionRelationType | undefined => {
|
): FieldDefinitionRelationType | undefined => {
|
||||||
if (field && field.type === 'RELATION') {
|
if (!field || field.type !== FieldMetadataType.Relation) return;
|
||||||
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';
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
const config: Record<
|
||||||
`Cannot determine field relation type for field : ${JSON.stringify(
|
RelationMetadataType,
|
||||||
field,
|
{ from: FieldDefinitionRelationType; to: FieldDefinitionRelationType }
|
||||||
)}.`,
|
> = {
|
||||||
);
|
[RelationMetadataType.ManyToMany]: {
|
||||||
} else {
|
from: 'FROM_MANY_OBJECTS',
|
||||||
return undefined;
|
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 { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
import { parseFieldType } from '@/object-metadata/utils/parseFieldType';
|
import { parseFieldType } from '@/object-metadata/utils/parseFieldType';
|
||||||
import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords';
|
|
||||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||||
import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon';
|
|
||||||
import { FieldDisplay } from '@/ui/object/field/components/FieldDisplay';
|
import { FieldDisplay } from '@/ui/object/field/components/FieldDisplay';
|
||||||
import { FieldContext } from '@/ui/object/field/contexts/FieldContext';
|
import { FieldContext } from '@/ui/object/field/contexts/FieldContext';
|
||||||
import { BooleanFieldInput } from '@/ui/object/field/meta-types/input/components/BooleanFieldInput';
|
import { BooleanFieldInput } from '@/ui/object/field/meta-types/input/components/BooleanFieldInput';
|
||||||
import { entityFieldsFamilySelector } from '@/ui/object/field/states/selectors/entityFieldsFamilySelector';
|
import { Field } from '~/generated/graphql';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { assertNotNull } from '~/utils/assert';
|
|
||||||
|
|
||||||
|
import { SettingsObjectFieldPreviewValueEffect } from '../components/SettingsObjectFieldPreviewValueEffect';
|
||||||
import { dataTypes } from '../constants/dataTypes';
|
import { dataTypes } from '../constants/dataTypes';
|
||||||
|
import { useFieldPreview } from '../hooks/useFieldPreview';
|
||||||
|
import { useRelationFieldPreview } from '../hooks/useRelationFieldPreview';
|
||||||
|
|
||||||
export type SettingsObjectFieldPreviewProps = {
|
export type SettingsObjectFieldPreviewProps = {
|
||||||
fieldIconKey?: string | null;
|
className?: string;
|
||||||
fieldLabel: string;
|
fieldMetadata: Pick<Field, 'icon' | 'label' | 'type'> & { id?: string };
|
||||||
fieldName?: string;
|
objectMetadataId: string;
|
||||||
fieldType: FieldMetadataType;
|
relationObjectMetadataId?: string;
|
||||||
isObjectCustom: boolean;
|
shrink?: boolean;
|
||||||
objectIconKey?: string | null;
|
|
||||||
objectLabelPlural: string;
|
|
||||||
objectNamePlural: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -52,7 +47,7 @@ const StyledObjectName = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledFieldPreview = styled.div`
|
const StyledFieldPreview = styled.div<{ shrink?: boolean }>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: ${({ theme }) => theme.background.primary};
|
background-color: ${({ theme }) => theme.background.primary};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
@ -61,7 +56,8 @@ const StyledFieldPreview = styled.div`
|
|||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
height: ${({ theme }) => theme.spacing(8)};
|
height: ${({ theme }) => theme.spacing(8)};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
padding: 0
|
||||||
|
${({ shrink, theme }) => (shrink ? theme.spacing(1) : theme.spacing(2))};
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -73,41 +69,41 @@ const StyledFieldLabel = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsObjectFieldPreview = ({
|
export const SettingsObjectFieldPreview = ({
|
||||||
fieldIconKey,
|
className,
|
||||||
fieldLabel,
|
fieldMetadata,
|
||||||
fieldName,
|
objectMetadataId,
|
||||||
fieldType,
|
relationObjectMetadataId,
|
||||||
isObjectCustom,
|
shrink,
|
||||||
objectIconKey,
|
|
||||||
objectLabelPlural,
|
|
||||||
objectNamePlural,
|
|
||||||
}: SettingsObjectFieldPreviewProps) => {
|
}: SettingsObjectFieldPreviewProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { Icon: ObjectIcon } = useLazyLoadIcon(objectIconKey ?? '');
|
|
||||||
const { Icon: FieldIcon } = useLazyLoadIcon(fieldIconKey ?? '');
|
|
||||||
|
|
||||||
const { objects } = useFindManyObjectRecords({
|
const {
|
||||||
objectNamePlural,
|
entityId,
|
||||||
skip: !fieldName,
|
FieldIcon,
|
||||||
|
fieldName,
|
||||||
|
hasValue,
|
||||||
|
ObjectIcon,
|
||||||
|
objectMetadataItem,
|
||||||
|
value,
|
||||||
|
} = useFieldPreview({
|
||||||
|
fieldMetadata,
|
||||||
|
objectMetadataId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [fieldValue, setFieldValue] = useRecoilState(
|
const { defaultValue: relationDefaultValue, entityChipDisplayMapper } =
|
||||||
entityFieldsFamilySelector({
|
useRelationFieldPreview({
|
||||||
entityId: objects[0]?.id ?? objectNamePlural,
|
relationObjectMetadataId,
|
||||||
fieldName: fieldName || 'new-field',
|
skipDefaultValue:
|
||||||
}),
|
fieldMetadata.type !== FieldMetadataType.Relation || hasValue,
|
||||||
);
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const defaultValue =
|
||||||
setFieldValue(
|
fieldMetadata.type === FieldMetadataType.Relation
|
||||||
fieldName && assertNotNull(objects[0]?.[fieldName])
|
? relationDefaultValue
|
||||||
? objects[0][fieldName]
|
: dataTypes[fieldMetadata.type].defaultValue;
|
||||||
: dataTypes[fieldType].defaultValue,
|
|
||||||
);
|
|
||||||
}, [fieldName, fieldType, fieldValue, objects, setFieldValue]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer className={className}>
|
||||||
<StyledObjectSummary>
|
<StyledObjectSummary>
|
||||||
<StyledObjectName>
|
<StyledObjectName>
|
||||||
{!!ObjectIcon && (
|
{!!ObjectIcon && (
|
||||||
@ -116,15 +112,20 @@ export const SettingsObjectFieldPreview = ({
|
|||||||
stroke={theme.icon.stroke.sm}
|
stroke={theme.icon.stroke.sm}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{objectLabelPlural}
|
{objectMetadataItem?.labelPlural}
|
||||||
</StyledObjectName>
|
</StyledObjectName>
|
||||||
{isObjectCustom ? (
|
{objectMetadataItem?.isCustom ? (
|
||||||
<Tag color="orange" text="Custom" />
|
<Tag color="orange" text="Custom" />
|
||||||
) : (
|
) : (
|
||||||
<Tag color="blue" text="Standard" />
|
<Tag color="blue" text="Standard" />
|
||||||
)}
|
)}
|
||||||
</StyledObjectSummary>
|
</StyledObjectSummary>
|
||||||
<StyledFieldPreview>
|
<SettingsObjectFieldPreviewValueEffect
|
||||||
|
entityId={entityId}
|
||||||
|
fieldName={fieldName}
|
||||||
|
value={value ?? defaultValue}
|
||||||
|
/>
|
||||||
|
<StyledFieldPreview shrink={shrink}>
|
||||||
<StyledFieldLabel>
|
<StyledFieldLabel>
|
||||||
{!!FieldIcon && (
|
{!!FieldIcon && (
|
||||||
<FieldIcon
|
<FieldIcon
|
||||||
@ -132,22 +133,26 @@ export const SettingsObjectFieldPreview = ({
|
|||||||
stroke={theme.icon.stroke.sm}
|
stroke={theme.icon.stroke.sm}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{fieldLabel}:
|
{fieldMetadata.label}:
|
||||||
</StyledFieldLabel>
|
</StyledFieldLabel>
|
||||||
<FieldContext.Provider
|
<FieldContext.Provider
|
||||||
value={{
|
value={{
|
||||||
entityId: objects[0]?.id ?? objectNamePlural,
|
entityId,
|
||||||
fieldDefinition: {
|
fieldDefinition: {
|
||||||
type: parseFieldType(fieldType as FieldMetadataType),
|
type: parseFieldType(fieldMetadata.type),
|
||||||
Icon: FieldIcon,
|
Icon: FieldIcon,
|
||||||
fieldMetadataId: '',
|
fieldMetadataId: fieldMetadata.id || '',
|
||||||
label: fieldLabel,
|
label: fieldMetadata.label,
|
||||||
metadata: { fieldName: fieldName || 'new-field' },
|
metadata: { fieldName },
|
||||||
|
entityChipDisplayMapper:
|
||||||
|
fieldMetadata.type === FieldMetadataType.Relation
|
||||||
|
? entityChipDisplayMapper
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
hotkeyScope: 'field-preview',
|
hotkeyScope: 'field-preview',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fieldType === 'BOOLEAN' ? (
|
{fieldMetadata.type === FieldMetadataType.Boolean ? (
|
||||||
<BooleanFieldInput readonly />
|
<BooleanFieldInput readonly />
|
||||||
) : (
|
) : (
|
||||||
<FieldDisplay />
|
<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)};
|
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledPreviewContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
const StyledFormContainer = styled.div`
|
const StyledFormContainer = styled.div`
|
||||||
background-color: ${({ theme }) => theme.background.secondary};
|
background-color: ${({ theme }) => theme.background.secondary};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
@ -46,7 +51,7 @@ export const SettingsObjectFieldTypeCard = ({
|
|||||||
<div className={className}>
|
<div className={className}>
|
||||||
<StyledPreviewContainer>
|
<StyledPreviewContainer>
|
||||||
<StyledTitle>Preview</StyledTitle>
|
<StyledTitle>Preview</StyledTitle>
|
||||||
{preview}
|
<StyledPreviewContent>{preview}</StyledPreviewContent>
|
||||||
</StyledPreviewContainer>
|
</StyledPreviewContainer>
|
||||||
{!!form && <StyledFormContainer>{form}</StyledFormContainer>}
|
{!!form && <StyledFormContainer>{form}</StyledFormContainer>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,82 +3,142 @@ import styled from '@emotion/styled';
|
|||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||||
import { Select } from '@/ui/input/components/Select';
|
import { Select } from '@/ui/input/components/Select';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
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 { dataTypes } from '../constants/dataTypes';
|
||||||
|
import { relationTypes } from '../constants/relationTypes';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SettingsObjectFieldPreview,
|
SettingsObjectFieldPreview,
|
||||||
SettingsObjectFieldPreviewProps,
|
SettingsObjectFieldPreviewProps,
|
||||||
} from './SettingsObjectFieldPreview';
|
} from './SettingsObjectFieldPreview';
|
||||||
|
import {
|
||||||
|
SettingsObjectFieldRelationForm,
|
||||||
|
SettingsObjectFieldRelationFormValues,
|
||||||
|
} from './SettingsObjectFieldRelationForm';
|
||||||
import { SettingsObjectFieldTypeCard } from './SettingsObjectFieldTypeCard';
|
import { SettingsObjectFieldTypeCard } from './SettingsObjectFieldTypeCard';
|
||||||
|
|
||||||
|
export type SettingsObjectFieldTypeSelectSectionFormValues = Partial<{
|
||||||
|
type: FieldMetadataType;
|
||||||
|
relation: SettingsObjectFieldRelationFormValues;
|
||||||
|
}>;
|
||||||
|
|
||||||
type SettingsObjectFieldTypeSelectSectionProps = {
|
type SettingsObjectFieldTypeSelectSectionProps = {
|
||||||
disabled?: boolean;
|
fieldMetadata: Pick<Field, 'icon' | 'label'> & { id?: string };
|
||||||
onChange?: (value: FieldMetadataType) => void;
|
relationFieldMetadataId?: string;
|
||||||
} & Pick<
|
onChange: (values: SettingsObjectFieldTypeSelectSectionFormValues) => void;
|
||||||
SettingsObjectFieldPreviewProps,
|
values?: SettingsObjectFieldTypeSelectSectionFormValues;
|
||||||
| 'fieldIconKey'
|
} & Pick<SettingsObjectFieldPreviewProps, 'objectMetadataId'>;
|
||||||
| 'fieldLabel'
|
|
||||||
| 'fieldName'
|
|
||||||
| 'fieldType'
|
|
||||||
| 'isObjectCustom'
|
|
||||||
| 'objectIconKey'
|
|
||||||
| 'objectLabelPlural'
|
|
||||||
| 'objectNamePlural'
|
|
||||||
>;
|
|
||||||
|
|
||||||
const StyledSettingsObjectFieldTypeCard = styled(SettingsObjectFieldTypeCard)`
|
const StyledSettingsObjectFieldTypeCard = styled(SettingsObjectFieldTypeCard)`
|
||||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// TODO: remove "enum" and "relation" types for now, add them back when the backend is ready.
|
const StyledSettingsObjectFieldPreview = styled(SettingsObjectFieldPreview)`
|
||||||
const { ENUM: _ENUM, RELATION: _RELATION, ...allowedDataTypes } = dataTypes;
|
display: grid;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRelationImage = styled.img<{ flip?: boolean }>`
|
||||||
|
transform: ${({ flip }) => (flip ? 'scaleX(-1)' : 'none')};
|
||||||
|
width: 54px;
|
||||||
|
`;
|
||||||
|
|
||||||
export const SettingsObjectFieldTypeSelectSection = ({
|
export const SettingsObjectFieldTypeSelectSection = ({
|
||||||
disabled,
|
fieldMetadata,
|
||||||
fieldIconKey,
|
relationFieldMetadataId,
|
||||||
fieldLabel,
|
objectMetadataId,
|
||||||
fieldName,
|
|
||||||
fieldType,
|
|
||||||
isObjectCustom,
|
|
||||||
objectIconKey,
|
|
||||||
objectLabelPlural,
|
|
||||||
objectNamePlural,
|
|
||||||
onChange,
|
onChange,
|
||||||
}: SettingsObjectFieldTypeSelectSectionProps) => (
|
values,
|
||||||
<Section>
|
}: SettingsObjectFieldTypeSelectSectionProps) => {
|
||||||
<H2Title
|
const relationFormConfig = values?.relation;
|
||||||
title="Type and values"
|
|
||||||
description="The field's type and values."
|
const allowedFieldTypes = Object.entries(dataTypes).filter(
|
||||||
/>
|
([key]) => key !== FieldMetadataType.Relation,
|
||||||
<Select
|
);
|
||||||
disabled={disabled}
|
|
||||||
dropdownScopeId="object-field-type-select"
|
return (
|
||||||
value={fieldType}
|
<Section>
|
||||||
onChange={onChange}
|
<H2Title
|
||||||
options={Object.entries(allowedDataTypes).map(([key, dataType]) => ({
|
title="Type and values"
|
||||||
value: key as FieldMetadataType,
|
description="The field's type and values."
|
||||||
...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}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<Select
|
||||||
</Section>
|
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 { Meta, StoryObj } from '@storybook/react';
|
||||||
import { userEvent, within } from '@storybook/testing-library';
|
import { userEvent, within } from '@storybook/testing-library';
|
||||||
|
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||||
|
|
||||||
import { SettingsObjectFieldTypeSelectSection } from '../SettingsObjectFieldTypeSelectSection';
|
import { SettingsObjectFieldTypeSelectSection } from '../SettingsObjectFieldTypeSelectSection';
|
||||||
@ -10,16 +9,7 @@ const meta: Meta<typeof SettingsObjectFieldTypeSelectSection> = {
|
|||||||
title: 'Modules/Settings/DataModel/SettingsObjectFieldTypeSelectSection',
|
title: 'Modules/Settings/DataModel/SettingsObjectFieldTypeSelectSection',
|
||||||
component: SettingsObjectFieldTypeSelectSection,
|
component: SettingsObjectFieldTypeSelectSection,
|
||||||
decorators: [ComponentDecorator],
|
decorators: [ComponentDecorator],
|
||||||
args: {
|
args: {},
|
||||||
fieldType: FieldMetadataType.Number,
|
|
||||||
fieldIconKey: 'IconUsers',
|
|
||||||
fieldLabel: 'Employees',
|
|
||||||
fieldName: 'employees',
|
|
||||||
isObjectCustom: false,
|
|
||||||
objectIconKey: 'IconUser',
|
|
||||||
objectLabelPlural: 'People',
|
|
||||||
objectNamePlural: 'people',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -28,7 +18,7 @@ type Story = StoryObj<typeof SettingsObjectFieldTypeSelectSection>;
|
|||||||
export const Default: Story = {};
|
export const Default: Story = {};
|
||||||
|
|
||||||
export const Disabled: Story = {
|
export const Disabled: Story = {
|
||||||
args: { disabled: true },
|
args: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithOpenSelect: Story = {
|
export const WithOpenSelect: Story = {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
IconMail,
|
IconMail,
|
||||||
IconNumbers,
|
IconNumbers,
|
||||||
IconPhone,
|
IconPhone,
|
||||||
IconPlug,
|
IconRelationManyToMany,
|
||||||
IconTag,
|
IconTag,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
IconUser,
|
IconUser,
|
||||||
@ -61,9 +61,12 @@ export const dataTypes: Record<
|
|||||||
[FieldMetadataType.Currency]: {
|
[FieldMetadataType.Currency]: {
|
||||||
label: 'Currency',
|
label: 'Currency',
|
||||||
Icon: IconCoins,
|
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.Email]: { label: 'Email', Icon: IconMail },
|
||||||
[FieldMetadataType.Phone]: { label: 'Phone', Icon: IconPhone },
|
[FieldMetadataType.Phone]: { label: 'Phone', Icon: IconPhone },
|
||||||
[FieldMetadataType.Probability]: {
|
[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)};
|
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||||
|
|
||||||
${({ theme, value }) =>
|
${({ theme, value }) =>
|
||||||
value === 'RELATION'
|
value === FieldMetadataType.Relation
|
||||||
? css`
|
? css`
|
||||||
border-color: ${theme.color.purple20};
|
border-color: ${theme.color.purple20};
|
||||||
color: ${theme.color.purple};
|
color: ${theme.color.purple};
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|||||||
import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon';
|
import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon';
|
||||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
import { dataTypes } from '../../constants/dataTypes';
|
import { dataTypes } from '../../constants/dataTypes';
|
||||||
|
|
||||||
@ -31,9 +30,6 @@ const StyledIconTableCell = styled(TableCell)`
|
|||||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
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 = ({
|
export const SettingsObjectFieldItemTableRow = ({
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
fieldItem,
|
fieldItem,
|
||||||
@ -42,13 +38,11 @@ export const SettingsObjectFieldItemTableRow = ({
|
|||||||
const { Icon } = useLazyLoadIcon(fieldItem.icon ?? '');
|
const { Icon } = useLazyLoadIcon(fieldItem.icon ?? '');
|
||||||
|
|
||||||
// TODO: parse with zod and merge types with FieldType (create a subset of FieldType for example)
|
// TODO: parse with zod and merge types with FieldType (create a subset of FieldType for example)
|
||||||
const fieldDataTypeIsSupported = Object.keys(
|
const fieldDataTypeIsSupported = Object.keys(dataTypes).includes(
|
||||||
dataTypesWithoutRelation,
|
fieldItem.type,
|
||||||
).includes(fieldItem.type);
|
);
|
||||||
|
|
||||||
if (!fieldDataTypeIsSupported) {
|
if (!fieldDataTypeIsSupported) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledObjectFieldTableRow>
|
<StyledObjectFieldTableRow>
|
||||||
@ -58,9 +52,7 @@ export const SettingsObjectFieldItemTableRow = ({
|
|||||||
</StyledNameTableCell>
|
</StyledNameTableCell>
|
||||||
<TableCell>{fieldItem.isCustom ? 'Custom' : 'Standard'}</TableCell>
|
<TableCell>{fieldItem.isCustom ? 'Custom' : 'Standard'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<SettingsObjectFieldDataType
|
<SettingsObjectFieldDataType value={fieldItem.type} />
|
||||||
value={fieldItem.type as FieldMetadataType}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<StyledIconTableCell>{ActionIcon}</StyledIconTableCell>
|
<StyledIconTableCell>{ActionIcon}</StyledIconTableCell>
|
||||||
</StyledObjectFieldTableRow>
|
</StyledObjectFieldTableRow>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import * as React from 'react';
|
import { MouseEvent, ReactNode } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { OverflowingTextWithTooltip } from '../../tooltip/OverflowingTextWithTooltip';
|
import { OverflowingTextWithTooltip } from '../../tooltip/OverflowingTextWithTooltip';
|
||||||
@ -28,9 +28,10 @@ type ChipProps = {
|
|||||||
maxWidth?: string;
|
maxWidth?: string;
|
||||||
variant?: ChipVariant;
|
variant?: ChipVariant;
|
||||||
accent?: ChipAccent;
|
accent?: ChipAccent;
|
||||||
leftComponent?: React.ReactNode;
|
leftComponent?: ReactNode;
|
||||||
rightComponent?: React.ReactNode;
|
rightComponent?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div<Partial<ChipProps>>`
|
const StyledContainer = styled.div<Partial<ChipProps>>`
|
||||||
@ -125,6 +126,7 @@ export const Chip = ({
|
|||||||
accent = ChipAccent.TextPrimary,
|
accent = ChipAccent.TextPrimary,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
className,
|
className,
|
||||||
|
onClick,
|
||||||
}: ChipProps) => (
|
}: ChipProps) => (
|
||||||
<StyledContainer
|
<StyledContainer
|
||||||
data-testid="chip"
|
data-testid="chip"
|
||||||
@ -135,6 +137,7 @@ export const Chip = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={className}
|
className={className}
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{leftComponent}
|
{leftComponent}
|
||||||
<StyledLabel>
|
<StyledLabel>
|
||||||
|
|||||||
@ -45,31 +45,31 @@ export const EntityChip = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return isNonEmptyString(name) ? (
|
return isNonEmptyString(name) ? (
|
||||||
<div onClick={handleLinkClick}>
|
<Chip
|
||||||
<Chip
|
label={name}
|
||||||
label={name}
|
variant={
|
||||||
variant={
|
linkToEntity
|
||||||
linkToEntity
|
? variant === EntityChipVariant.Regular
|
||||||
? variant === EntityChipVariant.Regular
|
? ChipVariant.Highlighted
|
||||||
? ChipVariant.Highlighted
|
: ChipVariant.Regular
|
||||||
: ChipVariant.Regular
|
: ChipVariant.Transparent
|
||||||
: ChipVariant.Transparent
|
}
|
||||||
}
|
leftComponent={
|
||||||
leftComponent={
|
LeftIcon ? (
|
||||||
LeftIcon ? (
|
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||||
<LeftIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
) : (
|
||||||
) : (
|
<Avatar
|
||||||
<Avatar
|
avatarUrl={pictureUrl}
|
||||||
avatarUrl={pictureUrl}
|
colorId={entityId}
|
||||||
colorId={entityId}
|
placeholder={name}
|
||||||
placeholder={name}
|
size="sm"
|
||||||
size="sm"
|
type={avatarType}
|
||||||
type={avatarType}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
}
|
||||||
}
|
clickable={!!linkToEntity}
|
||||||
/>
|
onClick={handleLinkClick}
|
||||||
</div>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -69,6 +69,9 @@ export {
|
|||||||
IconPlug,
|
IconPlug,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconProgressCheck,
|
IconProgressCheck,
|
||||||
|
IconRelationManyToMany,
|
||||||
|
IconRelationOneToMany,
|
||||||
|
IconRelationOneToOne,
|
||||||
IconRepeat,
|
IconRepeat,
|
||||||
IconRobot,
|
IconRobot,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
|
|||||||
|
|
||||||
type IconPickerProps = {
|
type IconPickerProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
dropdownScopeId?: string;
|
||||||
onChange: (params: { iconKey: string; Icon: IconComponent }) => void;
|
onChange: (params: { iconKey: string; Icon: IconComponent }) => void;
|
||||||
selectedIconKey?: string;
|
selectedIconKey?: string;
|
||||||
onClickOutside?: () => void;
|
onClickOutside?: () => void;
|
||||||
@ -44,6 +45,7 @@ const convertIconKeyToLabel = (iconKey: string) =>
|
|||||||
|
|
||||||
export const IconPicker = ({
|
export const IconPicker = ({
|
||||||
disabled,
|
disabled,
|
||||||
|
dropdownScopeId = 'icon-picker',
|
||||||
onChange,
|
onChange,
|
||||||
selectedIconKey,
|
selectedIconKey,
|
||||||
onClickOutside,
|
onClickOutside,
|
||||||
@ -53,7 +55,7 @@ export const IconPicker = ({
|
|||||||
}: IconPickerProps) => {
|
}: IconPickerProps) => {
|
||||||
const [searchString, setSearchString] = useState('');
|
const [searchString, setSearchString] = useState('');
|
||||||
|
|
||||||
const { closeDropdown } = useDropdown({ dropdownScopeId: 'icon-picker' });
|
const { closeDropdown } = useDropdown({ dropdownScopeId });
|
||||||
|
|
||||||
const { icons, isLoadingIcons: isLoading } = useLazyLoadIcons();
|
const { icons, isLoadingIcons: isLoading } = useLazyLoadIcons();
|
||||||
|
|
||||||
@ -75,7 +77,7 @@ export const IconPicker = ({
|
|||||||
}, [icons, searchString, selectedIconKey]);
|
}, [icons, searchString, selectedIconKey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownScope dropdownScopeId="icon-picker">
|
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownHotkeyScope={{ scope: IconPickerHotkeyScope.IconPicker }}
|
dropdownHotkeyScope={{ scope: IconPickerHotkeyScope.IconPicker }}
|
||||||
clickableComponent={
|
clickableComponent={
|
||||||
|
|||||||
@ -12,14 +12,16 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
|||||||
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
|
||||||
|
|
||||||
export type SelectProps<Value extends string | number | null> = {
|
export type SelectProps<Value extends string | number | null> = {
|
||||||
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
dropdownScopeId: string;
|
dropdownScopeId: string;
|
||||||
|
label?: string;
|
||||||
onChange?: (value: Value) => void;
|
onChange?: (value: Value) => void;
|
||||||
options: { value: Value; label: string; Icon?: IconComponent }[];
|
options: { value: Value; label: string; Icon?: IconComponent }[];
|
||||||
value?: Value;
|
value?: Value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div<{ disabled?: boolean }>`
|
const StyledControlContainer = styled.div<{ disabled?: boolean }>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
@ -34,7 +36,16 @@ const StyledContainer = styled.div<{ disabled?: boolean }>`
|
|||||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
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;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
@ -46,8 +57,10 @@ const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const Select = <Value extends string | number | null>({
|
export const Select = <Value extends string | number | null>({
|
||||||
|
className,
|
||||||
disabled,
|
disabled,
|
||||||
dropdownScopeId,
|
dropdownScopeId,
|
||||||
|
label,
|
||||||
onChange,
|
onChange,
|
||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
@ -59,46 +72,49 @@ export const Select = <Value extends string | number | null>({
|
|||||||
const { closeDropdown } = useDropdown({ dropdownScopeId });
|
const { closeDropdown } = useDropdown({ dropdownScopeId });
|
||||||
|
|
||||||
const selectControl = (
|
const selectControl = (
|
||||||
<StyledContainer disabled={disabled}>
|
<StyledControlContainer disabled={disabled}>
|
||||||
<StyledLabel>
|
<StyledControlLabel>
|
||||||
{!!selectedOption.Icon && (
|
{!!selectedOption?.Icon && (
|
||||||
<selectedOption.Icon
|
<selectedOption.Icon
|
||||||
color={disabled ? theme.font.color.light : theme.font.color.primary}
|
color={disabled ? theme.font.color.light : theme.font.color.primary}
|
||||||
size={theme.icon.size.md}
|
size={theme.icon.size.md}
|
||||||
stroke={theme.icon.stroke.sm}
|
stroke={theme.icon.stroke.sm}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{selectedOption.label}
|
{selectedOption?.label}
|
||||||
</StyledLabel>
|
</StyledControlLabel>
|
||||||
<StyledIconChevronDown disabled={disabled} size={theme.icon.size.md} />
|
<StyledIconChevronDown disabled={disabled} size={theme.icon.size.md} />
|
||||||
</StyledContainer>
|
</StyledControlContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
return disabled ? (
|
return disabled ? (
|
||||||
selectControl
|
selectControl
|
||||||
) : (
|
) : (
|
||||||
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
||||||
<Dropdown
|
<div className={className}>
|
||||||
dropdownMenuWidth={176}
|
{!!label && <StyledLabel>{label}</StyledLabel>}
|
||||||
dropdownPlacement="bottom-start"
|
<Dropdown
|
||||||
clickableComponent={selectControl}
|
dropdownMenuWidth={176}
|
||||||
dropdownComponents={
|
dropdownPlacement="bottom-start"
|
||||||
<DropdownMenuItemsContainer>
|
clickableComponent={selectControl}
|
||||||
{options.map((option) => (
|
dropdownComponents={
|
||||||
<MenuItem
|
<DropdownMenuItemsContainer>
|
||||||
key={option.value}
|
{options.map((option) => (
|
||||||
LeftIcon={option.Icon}
|
<MenuItem
|
||||||
text={option.label}
|
key={option.value}
|
||||||
onClick={() => {
|
LeftIcon={option.Icon}
|
||||||
onChange?.(option.value);
|
text={option.label}
|
||||||
closeDropdown();
|
onClick={() => {
|
||||||
}}
|
onChange?.(option.value);
|
||||||
/>
|
closeDropdown();
|
||||||
))}
|
}}
|
||||||
</DropdownMenuItemsContainer>
|
/>
|
||||||
}
|
))}
|
||||||
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
|
</DropdownMenuItemsContainer>
|
||||||
/>
|
}
|
||||||
|
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</DropdownScope>
|
</DropdownScope>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,8 +5,6 @@ const StyledPanel = styled.div`
|
|||||||
background: ${({ theme }) => theme.background.primary};
|
background: ${({ theme }) => theme.background.primary};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@ -7,9 +7,10 @@ export const getEntityChipFromFieldMetadata = (
|
|||||||
fieldDefinition: FieldDefinition<FieldRelationMetadata>,
|
fieldDefinition: FieldDefinition<FieldRelationMetadata>,
|
||||||
fieldValue: any,
|
fieldValue: any,
|
||||||
) => {
|
) => {
|
||||||
|
const { entityChipDisplayMapper } = fieldDefinition;
|
||||||
const { fieldName } = fieldDefinition.metadata;
|
const { fieldName } = fieldDefinition.metadata;
|
||||||
|
|
||||||
const chipValue: Pick<
|
const defaultChipValue: Pick<
|
||||||
EntityChipProps,
|
EntityChipProps,
|
||||||
'name' | 'pictureUrl' | 'avatarType' | 'entityId'
|
'name' | 'pictureUrl' | 'avatarType' | 'entityId'
|
||||||
> = {
|
> = {
|
||||||
@ -19,15 +20,23 @@ export const getEntityChipFromFieldMetadata = (
|
|||||||
entityId: fieldValue?.id,
|
entityId: fieldValue?.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: use every
|
if (['accountOwner', 'person'].includes(fieldName) && fieldValue) {
|
||||||
if (fieldName === 'accountOwner' && fieldValue) {
|
return {
|
||||||
chipValue.name = fieldValue.name.firstName + ' ' + fieldValue.name.lastName;
|
...defaultChipValue,
|
||||||
} else if (fieldName === 'company' && fieldValue) {
|
name: `${fieldValue.firstName} ${fieldValue.lastName}`,
|
||||||
chipValue.name = fieldValue.name;
|
};
|
||||||
chipValue.pictureUrl = getLogoUrlFromDomainName(fieldValue.domainName);
|
|
||||||
} else if (fieldName === 'person' && fieldValue) {
|
|
||||||
chipValue.name = fieldValue.name.firstName + ' ' + fieldValue.name.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';
|
import { FieldType } from './FieldType';
|
||||||
|
|
||||||
export type FieldDefinitionRelationType =
|
export type FieldDefinitionRelationType =
|
||||||
| 'TO_ONE_OBJECT'
|
| 'FROM_MANY_OBJECTS'
|
||||||
| 'FROM_NAMY_OBJECTS'
|
| 'FROM_ONE_OBJECT'
|
||||||
| 'TO_MANY_OBJECTS';
|
| 'TO_MANY_OBJECTS'
|
||||||
|
| 'TO_ONE_OBJECT';
|
||||||
|
|
||||||
export type FieldDefinition<T extends FieldMetadata> = {
|
export type FieldDefinition<T extends FieldMetadata> = {
|
||||||
fieldMetadataId: string;
|
fieldMetadataId: string;
|
||||||
|
|||||||
@ -1,8 +1,20 @@
|
|||||||
import { isNull, isString } from '@sniptt/guards';
|
import { isNull, isString } from '@sniptt/guards';
|
||||||
|
|
||||||
|
import { formatToHumanReadableDate } from '~/utils';
|
||||||
|
|
||||||
import { FieldDateValue } from '../FieldMetadata';
|
import { FieldDateValue } from '../FieldMetadata';
|
||||||
|
|
||||||
// TODO: add zod
|
// TODO: add zod
|
||||||
export const isFieldDateValue = (
|
export const isFieldDateValue = (
|
||||||
fieldValue: unknown,
|
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 { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||||
|
import { useRelationMetadata } from '@/object-metadata/hooks/useRelationMetadata';
|
||||||
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
|
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
|
||||||
import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection';
|
import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection';
|
||||||
|
import { useFieldMetadataForm } from '@/settings/data-model/hooks/useFieldMetadataForm';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { IconArchive, IconSettings } from '@/ui/display/icon';
|
import { IconArchive, IconSettings } from '@/ui/display/icon';
|
||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||||
@ -34,13 +36,22 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
metadataField.isActive && getFieldSlug(metadataField) === fieldSlug,
|
metadataField.isActive && getFieldSlug(metadataField) === fieldSlug,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [formValues, setFormValues] = useState<
|
const {
|
||||||
Partial<{
|
relationFieldMetadataItem,
|
||||||
icon: string;
|
relationObjectMetadataItem,
|
||||||
label: string;
|
relationType,
|
||||||
description: string;
|
} = useRelationMetadata({ fieldMetadataItem: activeMetadataField });
|
||||||
}>
|
|
||||||
>({});
|
const {
|
||||||
|
formValues,
|
||||||
|
handleFormChange,
|
||||||
|
hasFieldFormChanged,
|
||||||
|
hasFormChanged,
|
||||||
|
hasRelationFormChanged,
|
||||||
|
initForm,
|
||||||
|
isValid,
|
||||||
|
validatedFormValues,
|
||||||
|
} = useFieldMetadataForm();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
@ -50,36 +61,59 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Object.keys(formValues).length) {
|
initForm({
|
||||||
setFormValues({
|
icon: activeMetadataField.icon ?? undefined,
|
||||||
icon: activeMetadataField.icon ?? undefined,
|
label: activeMetadataField.label,
|
||||||
label: activeMetadataField.label,
|
description: activeMetadataField.description ?? undefined,
|
||||||
description: activeMetadataField.description ?? undefined,
|
type: activeMetadataField.type,
|
||||||
});
|
relation: {
|
||||||
}
|
field: {
|
||||||
|
icon: relationFieldMetadataItem?.icon,
|
||||||
|
label: relationFieldMetadataItem?.label,
|
||||||
|
},
|
||||||
|
objectMetadataId: relationObjectMetadataItem?.id,
|
||||||
|
type: relationType,
|
||||||
|
},
|
||||||
|
});
|
||||||
}, [
|
}, [
|
||||||
activeMetadataField,
|
activeMetadataField,
|
||||||
activeObjectMetadataItem,
|
activeObjectMetadataItem,
|
||||||
formValues,
|
initForm,
|
||||||
loading,
|
loading,
|
||||||
navigate,
|
navigate,
|
||||||
|
relationFieldMetadataItem?.icon,
|
||||||
|
relationFieldMetadataItem?.label,
|
||||||
|
relationObjectMetadataItem?.id,
|
||||||
|
relationType,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!activeObjectMetadataItem || !activeMetadataField) return null;
|
if (!activeObjectMetadataItem || !activeMetadataField) return null;
|
||||||
|
|
||||||
const areRequiredFieldsFilled = !!formValues.label;
|
const canSave = isValid && hasFormChanged;
|
||||||
|
|
||||||
const hasChanges =
|
|
||||||
formValues.description !== activeMetadataField.description ||
|
|
||||||
formValues.icon !== activeMetadataField.icon ||
|
|
||||||
formValues.label !== activeMetadataField.label;
|
|
||||||
|
|
||||||
const canSave = areRequiredFieldsFilled && hasChanges;
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
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}`);
|
navigate(`/settings/objects/${objectSlug}`);
|
||||||
};
|
};
|
||||||
@ -116,23 +150,21 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
name={formValues.label}
|
name={formValues.label}
|
||||||
description={formValues.description}
|
description={formValues.description}
|
||||||
iconKey={formValues.icon}
|
iconKey={formValues.icon}
|
||||||
onChange={(values) =>
|
onChange={handleFormChange}
|
||||||
setFormValues((previousFormValues) => ({
|
|
||||||
...previousFormValues,
|
|
||||||
...values,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<SettingsObjectFieldTypeSelectSection
|
<SettingsObjectFieldTypeSelectSection
|
||||||
disabled
|
fieldMetadata={{
|
||||||
fieldIconKey={formValues.icon}
|
icon: formValues.icon,
|
||||||
fieldLabel={formValues.label || 'Employees'}
|
label: formValues.label || 'Employees',
|
||||||
fieldName={activeMetadataField.name}
|
id: activeMetadataField.id,
|
||||||
fieldType={activeMetadataField.type as FieldMetadataType}
|
}}
|
||||||
isObjectCustom={activeObjectMetadataItem.isCustom}
|
objectMetadataId={activeObjectMetadataItem.id}
|
||||||
objectIconKey={activeObjectMetadataItem.icon}
|
onChange={handleFormChange}
|
||||||
objectLabelPlural={activeObjectMetadataItem.labelPlural}
|
relationFieldMetadataId={relationFieldMetadataItem?.id}
|
||||||
objectNamePlural={activeObjectMetadataItem.namePlural}
|
values={{
|
||||||
|
type: formValues.type,
|
||||||
|
relation: formValues.relation,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title title="Danger zone" description="Disable this field" />
|
<H2Title title="Danger zone" description="Disable this field" />
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useCreateOneRelationMetadata } from '@/object-metadata/hooks/useCreateOneRelationMetadata';
|
||||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||||
import { useCreateOneObjectRecord } from '@/object-record/hooks/useCreateOneObjectRecord';
|
import { useCreateOneObjectRecord } from '@/object-record/hooks/useCreateOneObjectRecord';
|
||||||
@ -11,6 +12,7 @@ import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderCon
|
|||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
|
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
|
||||||
import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection';
|
import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection';
|
||||||
|
import { useFieldMetadataForm } from '@/settings/data-model/hooks/useFieldMetadataForm';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { IconSettings } from '@/ui/display/icon';
|
import { IconSettings } from '@/ui/display/icon';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
@ -23,26 +25,48 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { objectSlug = '' } = useParams();
|
const { objectSlug = '' } = useParams();
|
||||||
|
|
||||||
const { findActiveObjectMetadataItemBySlug, loading } =
|
const {
|
||||||
useObjectMetadataItemForSettings();
|
findActiveObjectMetadataItemBySlug,
|
||||||
|
findObjectMetadataItemById,
|
||||||
|
findObjectMetadataItemByNamePlural,
|
||||||
|
loading,
|
||||||
|
} = useObjectMetadataItemForSettings();
|
||||||
|
|
||||||
const activeObjectMetadataItem =
|
const activeObjectMetadataItem =
|
||||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||||
const { createMetadataField } = useFieldMetadataItem();
|
const { createMetadataField } = useFieldMetadataItem();
|
||||||
|
|
||||||
|
const {
|
||||||
|
formValues,
|
||||||
|
handleFormChange,
|
||||||
|
initForm,
|
||||||
|
isValid: canSave,
|
||||||
|
validatedFormValues,
|
||||||
|
} = useFieldMetadataForm();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
if (!activeObjectMetadataItem) navigate(AppPath.NotFound);
|
if (!activeObjectMetadataItem) {
|
||||||
}, [activeObjectMetadataItem, loading, navigate]);
|
navigate(AppPath.NotFound);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [formValues, setFormValues] = useState<{
|
initForm({
|
||||||
description?: string;
|
relation: {
|
||||||
icon: string;
|
field: { icon: activeObjectMetadataItem.icon },
|
||||||
label: string;
|
objectMetadataId: findObjectMetadataItemByNamePlural('peopleV2')?.id,
|
||||||
type: FieldMetadataType;
|
},
|
||||||
}>({ icon: 'IconUsers', label: '', type: FieldMetadataType.Number });
|
});
|
||||||
|
}, [
|
||||||
|
activeObjectMetadataItem,
|
||||||
|
findObjectMetadataItemByNamePlural,
|
||||||
|
initForm,
|
||||||
|
loading,
|
||||||
|
navigate,
|
||||||
|
]);
|
||||||
|
|
||||||
const [objectViews, setObjectViews] = useState<View[]>([]);
|
const [objectViews, setObjectViews] = useState<View[]>([]);
|
||||||
|
const [relationObjectViews, setRelationObjectViews] = useState<View[]>([]);
|
||||||
|
|
||||||
const { createOneObject: createOneViewField } = useCreateOneObjectRecord({
|
const { createOneObject: createOneViewField } = useCreateOneObjectRecord({
|
||||||
objectNameSingular: 'viewFieldV2',
|
objectNameSingular: 'viewFieldV2',
|
||||||
@ -57,32 +81,100 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
onCompleted: async (data: PaginatedObjectTypeResults<View>) => {
|
onCompleted: async (data: PaginatedObjectTypeResults<View>) => {
|
||||||
const views = data.edges;
|
const views = data.edges;
|
||||||
|
|
||||||
if (!views) {
|
if (!views) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setObjectViews(data.edges.map(({ node }) => node));
|
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;
|
if (!activeObjectMetadataItem) return null;
|
||||||
|
|
||||||
const canSave = !!formValues.label;
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
const createdField = await createMetadataField({
|
if (!validatedFormValues) return;
|
||||||
...formValues,
|
|
||||||
objectMetadataId: activeObjectMetadataItem.id,
|
if (validatedFormValues.type === FieldMetadataType.Relation) {
|
||||||
});
|
const createdRelation = await createOneRelationMetadata({
|
||||||
objectViews.forEach(async (view) => {
|
relationType: validatedFormValues.relation.type,
|
||||||
await createOneViewField?.({
|
field: {
|
||||||
view: view.id,
|
description: validatedFormValues.description,
|
||||||
fieldMetadataId: createdField.data?.createOneField.id,
|
icon: validatedFormValues.icon,
|
||||||
position: activeObjectMetadataItem.fields.length,
|
label: validatedFormValues.label,
|
||||||
isVisible: true,
|
},
|
||||||
size: 100,
|
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}`);
|
navigate(`/settings/objects/${objectSlug}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -110,24 +202,19 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
iconKey={formValues.icon}
|
iconKey={formValues.icon}
|
||||||
name={formValues.label}
|
name={formValues.label}
|
||||||
description={formValues.description}
|
description={formValues.description}
|
||||||
onChange={(values) =>
|
onChange={handleFormChange}
|
||||||
setFormValues((previousValues) => ({
|
|
||||||
...previousValues,
|
|
||||||
...values,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<SettingsObjectFieldTypeSelectSection
|
<SettingsObjectFieldTypeSelectSection
|
||||||
fieldIconKey={formValues.icon}
|
fieldMetadata={{
|
||||||
fieldLabel={formValues.label || 'Employees'}
|
icon: formValues.icon,
|
||||||
fieldType={formValues.type}
|
label: formValues.label || 'Employees',
|
||||||
isObjectCustom={activeObjectMetadataItem.isCustom}
|
}}
|
||||||
objectIconKey={activeObjectMetadataItem.icon}
|
objectMetadataId={activeObjectMetadataItem.id}
|
||||||
objectLabelPlural={activeObjectMetadataItem.labelPlural}
|
onChange={handleFormChange}
|
||||||
objectNamePlural={activeObjectMetadataItem.namePlural}
|
values={{
|
||||||
onChange={(type) =>
|
type: formValues.type,
|
||||||
setFormValues((previousValues) => ({ ...previousValues, type }))
|
relation: formValues.relation,
|
||||||
}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsPageContainer>
|
</SettingsPageContainer>
|
||||||
</SubMenuTopBarContainer>
|
</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 = {
|
export const mockedObjectMetadataItems = {
|
||||||
edges: [
|
edges: [
|
||||||
{
|
{
|
||||||
@ -198,188 +382,8 @@ export const mockedObjectMetadataItems = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
mockedCompaniesMetadata,
|
||||||
node: {
|
mockedWorkspacesMetadata,
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
@Entity('workspaces')
|
@Entity('workspaceV2')
|
||||||
@ObjectType('workspace')
|
@ObjectType('workspaceV2')
|
||||||
export class Workspace {
|
export class Workspace {
|
||||||
@IDField(() => ID)
|
@IDField(() => ID)
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
|||||||
Reference in New Issue
Block a user