feat: add Object Edit Settings section with Object preview (#4216)
* feat: add Object Edit Settings section with Object preview Closes #3834 * fix: fix preview card stories * test: improve getFieldDefaultPreviewValue tests * test: add getFieldPreviewValueFromRecord tests * test: add useFieldPreview tests * refactor: rename and move components * fix: restore RecordStoreDecorator
This commit is contained in:
@ -62,9 +62,9 @@ export const getObjectRecordIdentifier = ({
|
||||
// TODO: This is a temporary solution before we seed imageIdentifierFieldMetadataId in the database
|
||||
const avatarUrl =
|
||||
(objectMetadataItem.nameSingular === CoreObjectNameSingular.Company
|
||||
? getLogoUrlFromDomainName(record['domainName'] ?? '')
|
||||
? getLogoUrlFromDomainName(record.domainName ?? '')
|
||||
: objectMetadataItem.nameSingular === CoreObjectNameSingular.Person
|
||||
? record['avatarUrl'] ?? ''
|
||||
? record.avatarUrl ?? ''
|
||||
: imageIdentifierFieldValue) ?? '';
|
||||
|
||||
const basePathToShowPage = getBasePathToShowPage({
|
||||
@ -74,9 +74,10 @@ export const getObjectRecordIdentifier = ({
|
||||
const isWorkspaceMemberObjectMetadata =
|
||||
objectMetadataItem.nameSingular === CoreObjectNameSingular.WorkspaceMember;
|
||||
|
||||
const linkToShowPage = isWorkspaceMemberObjectMetadata
|
||||
? ''
|
||||
: `${basePathToShowPage}${record.id}`;
|
||||
const linkToShowPage =
|
||||
isWorkspaceMemberObjectMetadata || !record.id
|
||||
? ''
|
||||
: `${basePathToShowPage}${record.id}`;
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
|
||||
export const fieldMetadataId = 'fieldMetadataId';
|
||||
|
||||
const mockedPersonObjectMetadataItem = {
|
||||
export const mockedPersonObjectMetadataItem = {
|
||||
...mockedPeopleMetadata.node,
|
||||
fields: mockedPeopleMetadata.node.fields.edges.map(({ node }) => node),
|
||||
};
|
||||
|
||||
@ -4,14 +4,12 @@ import styled from '@emotion/styled';
|
||||
import { Card } from '@/ui/layout/card/components/Card';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
|
||||
type SettingsObjectFieldTypeCardProps = {
|
||||
type SettingsDataModelPreviewFormCardProps = {
|
||||
className?: string;
|
||||
preview: ReactNode;
|
||||
form?: ReactNode;
|
||||
};
|
||||
|
||||
const StyledCard = styled(Card)``;
|
||||
|
||||
const StyledPreviewContainer = styled(CardContent)`
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
`;
|
||||
@ -24,27 +22,20 @@ const StyledTitle = styled.h3`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledPreviewContent = styled.div`
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
const StyledFormContainer = styled(CardContent)`
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export const SettingsObjectFieldTypeCard = ({
|
||||
export const SettingsDataModelPreviewFormCard = ({
|
||||
className,
|
||||
preview,
|
||||
form,
|
||||
}: SettingsObjectFieldTypeCardProps) => {
|
||||
return (
|
||||
<StyledCard className={className}>
|
||||
<StyledPreviewContainer divider={!!form}>
|
||||
<StyledTitle>Preview</StyledTitle>
|
||||
<StyledPreviewContent>{preview}</StyledPreviewContent>
|
||||
</StyledPreviewContainer>
|
||||
{!!form && <StyledFormContainer>{form}</StyledFormContainer>}
|
||||
</StyledCard>
|
||||
);
|
||||
};
|
||||
}: SettingsDataModelPreviewFormCardProps) => (
|
||||
<Card className={className} fullWidth>
|
||||
<StyledPreviewContainer divider={!!form}>
|
||||
<StyledTitle>Preview</StyledTitle>
|
||||
{preview}
|
||||
</StyledPreviewContainer>
|
||||
{!!form && <StyledFormContainer>{form}</StyledFormContainer>}
|
||||
</Card>
|
||||
);
|
||||
@ -1,165 +0,0 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { parseFieldType } from '@/object-metadata/utils/parseFieldType';
|
||||
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { BooleanFieldInput } from '@/object-record/record-field/meta-types/input/components/BooleanFieldInput';
|
||||
import { RatingFieldInput } from '@/object-record/record-field/meta-types/input/components/RatingFieldInput';
|
||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||
import { Card } from '@/ui/layout/card/components/Card';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { SettingsObjectFieldPreviewValueEffect } from '../components/SettingsObjectFieldPreviewValueEffect';
|
||||
import { useFieldPreview } from '../hooks/useFieldPreview';
|
||||
|
||||
import { SettingsObjectFieldSelectFormValues } from './SettingsObjectFieldSelectForm';
|
||||
|
||||
export type SettingsObjectFieldPreviewProps = {
|
||||
className?: string;
|
||||
fieldMetadata: Pick<Field, 'icon' | 'label' | 'type'> & { id?: string };
|
||||
objectMetadataId: string;
|
||||
relationObjectMetadataId?: string;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
shrink?: boolean;
|
||||
};
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
max-width: 480px;
|
||||
`;
|
||||
|
||||
const StyledCardContent = styled(CardContent)`
|
||||
display: grid;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledObjectSummary = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: space-between;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledObjectName = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledFieldPreview = styled.div<{ shrink?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
overflow: hidden;
|
||||
padding: 0
|
||||
${({ shrink, theme }) => (shrink ? theme.spacing(1) : theme.spacing(2))};
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledFieldLabel = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const SettingsObjectFieldPreview = ({
|
||||
className,
|
||||
fieldMetadata,
|
||||
objectMetadataId,
|
||||
relationObjectMetadataId,
|
||||
selectOptions,
|
||||
shrink,
|
||||
}: SettingsObjectFieldPreviewProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
entityId,
|
||||
FieldIcon,
|
||||
fieldName,
|
||||
ObjectIcon,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
value,
|
||||
} = useFieldPreview({
|
||||
fieldMetadata,
|
||||
objectMetadataId,
|
||||
relationObjectMetadataId,
|
||||
selectOptions,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledCard className={className}>
|
||||
<StyledCardContent>
|
||||
<StyledObjectSummary>
|
||||
<StyledObjectName>
|
||||
{!!ObjectIcon && (
|
||||
<ObjectIcon
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.md}
|
||||
/>
|
||||
)}
|
||||
{objectMetadataItem?.labelPlural}
|
||||
</StyledObjectName>
|
||||
{objectMetadataItem?.isCustom ? (
|
||||
<Tag color="orange" text="Custom" weight="medium" />
|
||||
) : (
|
||||
<Tag color="blue" text="Standard" weight="medium" />
|
||||
)}
|
||||
</StyledObjectSummary>
|
||||
<SettingsObjectFieldPreviewValueEffect
|
||||
entityId={entityId}
|
||||
fieldName={fieldName}
|
||||
value={value}
|
||||
/>
|
||||
<StyledFieldPreview shrink={shrink}>
|
||||
<StyledFieldLabel>
|
||||
{!!FieldIcon && (
|
||||
<FieldIcon
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
)}
|
||||
{fieldMetadata.label}:
|
||||
</StyledFieldLabel>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition: {
|
||||
type: parseFieldType(fieldMetadata.type),
|
||||
iconName: 'FieldIcon',
|
||||
fieldMetadataId: fieldMetadata.id || '',
|
||||
label: fieldMetadata.label,
|
||||
metadata: {
|
||||
fieldName,
|
||||
relationObjectMetadataNameSingular:
|
||||
relationObjectMetadataItem?.nameSingular,
|
||||
options: selectOptions,
|
||||
},
|
||||
},
|
||||
hotkeyScope: 'field-preview',
|
||||
}}
|
||||
>
|
||||
{fieldMetadata.type === FieldMetadataType.Boolean ? (
|
||||
<BooleanFieldInput readonly />
|
||||
) : fieldMetadata.type === FieldMetadataType.Rating ? (
|
||||
<RatingFieldInput readonly />
|
||||
) : (
|
||||
<FieldDisplay />
|
||||
)}
|
||||
</FieldContext.Provider>
|
||||
</StyledFieldPreview>
|
||||
</StyledCardContent>
|
||||
</StyledCard>
|
||||
);
|
||||
};
|
||||
@ -1,183 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes';
|
||||
import { SETTINGS_FIELD_METADATA_TYPES } from '@/settings/data-model/constants/SettingsFieldMetadataTypes';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import {
|
||||
SettingsObjectFieldCurrencyForm,
|
||||
SettingsObjectFieldCurrencyFormValues,
|
||||
} from './SettingsObjectFieldCurrencyForm';
|
||||
import {
|
||||
SettingsObjectFieldPreview,
|
||||
SettingsObjectFieldPreviewProps,
|
||||
} from './SettingsObjectFieldPreview';
|
||||
import {
|
||||
SettingsObjectFieldRelationForm,
|
||||
SettingsObjectFieldRelationFormValues,
|
||||
} from './SettingsObjectFieldRelationForm';
|
||||
import {
|
||||
SettingsObjectFieldSelectForm,
|
||||
SettingsObjectFieldSelectFormValues,
|
||||
} from './SettingsObjectFieldSelectForm';
|
||||
import { SettingsObjectFieldTypeCard } from './SettingsObjectFieldTypeCard';
|
||||
|
||||
export type SettingsObjectFieldTypeSelectSectionFormValues = {
|
||||
type: FieldMetadataType;
|
||||
currency: SettingsObjectFieldCurrencyFormValues;
|
||||
relation: SettingsObjectFieldRelationFormValues;
|
||||
select: SettingsObjectFieldSelectFormValues;
|
||||
};
|
||||
|
||||
type SettingsObjectFieldTypeSelectSectionProps = {
|
||||
disableCurrencyForm?: boolean;
|
||||
excludedFieldTypes?: FieldMetadataType[];
|
||||
fieldMetadata: Pick<Field, 'icon' | 'label'> & { id?: string };
|
||||
onChange: (
|
||||
values: Partial<SettingsObjectFieldTypeSelectSectionFormValues>,
|
||||
) => void;
|
||||
relationFieldMetadata?: Pick<Field, 'id' | 'isCustom'>;
|
||||
values: SettingsObjectFieldTypeSelectSectionFormValues;
|
||||
} & Pick<SettingsObjectFieldPreviewProps, 'objectMetadataId'>;
|
||||
|
||||
const StyledSettingsObjectFieldTypeCard = styled(SettingsObjectFieldTypeCard)`
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledSettingsObjectFieldPreview = styled(SettingsObjectFieldPreview)`
|
||||
display: grid;
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
|
||||
const StyledRelationImage = styled.img<{ flip?: boolean }>`
|
||||
transform: ${({ flip }) => (flip ? 'scaleX(-1)' : 'none')};
|
||||
width: 54px;
|
||||
`;
|
||||
|
||||
export const SettingsObjectFieldTypeSelectSection = ({
|
||||
disableCurrencyForm,
|
||||
excludedFieldTypes,
|
||||
fieldMetadata,
|
||||
objectMetadataId,
|
||||
onChange,
|
||||
relationFieldMetadata,
|
||||
values,
|
||||
}: SettingsObjectFieldTypeSelectSectionProps) => {
|
||||
const currencyFormConfig = values.currency;
|
||||
const relationFormConfig = values.relation;
|
||||
const selectFormConfig = values.select;
|
||||
|
||||
const fieldTypeOptions = Object.entries(SETTINGS_FIELD_METADATA_TYPES)
|
||||
.filter(([key]) => !excludedFieldTypes?.includes(key as FieldMetadataType))
|
||||
.map(([key, dataTypeConfig]) => ({
|
||||
value: key as FieldMetadataType,
|
||||
...dataTypeConfig,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Type and values"
|
||||
description="The field's type and values."
|
||||
/>
|
||||
<Select
|
||||
fullWidth
|
||||
disabled={!!fieldMetadata?.id}
|
||||
dropdownId="object-field-type-select"
|
||||
value={values?.type}
|
||||
onChange={(value) => onChange({ type: value })}
|
||||
options={fieldTypeOptions}
|
||||
/>
|
||||
{!!values?.type &&
|
||||
[
|
||||
FieldMetadataType.Boolean,
|
||||
FieldMetadataType.Currency,
|
||||
FieldMetadataType.DateTime,
|
||||
FieldMetadataType.Select,
|
||||
FieldMetadataType.Link,
|
||||
FieldMetadataType.Number,
|
||||
FieldMetadataType.Rating,
|
||||
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
|
||||
}
|
||||
selectOptions={selectFormConfig}
|
||||
/>
|
||||
{values.type === FieldMetadataType.Relation &&
|
||||
!!relationFormConfig?.type &&
|
||||
!!relationFormConfig.objectMetadataId && (
|
||||
<>
|
||||
<StyledRelationImage
|
||||
src={RELATION_TYPES[relationFormConfig.type].imageSrc}
|
||||
flip={
|
||||
RELATION_TYPES[relationFormConfig.type].isImageFlipped
|
||||
}
|
||||
alt={RELATION_TYPES[relationFormConfig.type].label}
|
||||
/>
|
||||
<StyledSettingsObjectFieldPreview
|
||||
fieldMetadata={{
|
||||
...relationFormConfig.field,
|
||||
label:
|
||||
relationFormConfig.field?.label || 'Field name',
|
||||
type: FieldMetadataType.Relation,
|
||||
id: relationFieldMetadata?.id,
|
||||
}}
|
||||
shrink
|
||||
objectMetadataId={relationFormConfig.objectMetadataId}
|
||||
relationObjectMetadataId={objectMetadataId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
form={
|
||||
values.type === FieldMetadataType.Currency ? (
|
||||
<SettingsObjectFieldCurrencyForm
|
||||
disabled={disableCurrencyForm}
|
||||
values={currencyFormConfig}
|
||||
onChange={(nextValues) =>
|
||||
onChange({
|
||||
currency: { ...currencyFormConfig, ...nextValues },
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : values.type === FieldMetadataType.Relation ? (
|
||||
<SettingsObjectFieldRelationForm
|
||||
disableFieldEdition={
|
||||
relationFieldMetadata && !relationFieldMetadata.isCustom
|
||||
}
|
||||
disableRelationEdition={!!relationFieldMetadata}
|
||||
values={relationFormConfig}
|
||||
onChange={(nextValues) =>
|
||||
onChange({
|
||||
relation: { ...relationFormConfig, ...nextValues },
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : values.type === FieldMetadataType.Select ? (
|
||||
<SettingsObjectFieldSelectForm
|
||||
values={selectFormConfig}
|
||||
onChange={(nextValues) => onChange({ select: nextValues })}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -1,128 +0,0 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { RecordStoreDecorator } from '~/testing/decorators/RecordStoreDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import {
|
||||
mockedCompaniesMetadata,
|
||||
mockedPeopleMetadata,
|
||||
} from '~/testing/mock-data/metadata';
|
||||
|
||||
import { SettingsObjectFieldPreview } from '../SettingsObjectFieldPreview';
|
||||
|
||||
const meta: Meta<typeof SettingsObjectFieldPreview> = {
|
||||
title: 'Modules/Settings/DataModel/SettingsObjectFieldPreview',
|
||||
component: SettingsObjectFieldPreview,
|
||||
decorators: [
|
||||
RecordStoreDecorator,
|
||||
ComponentDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
SnackBarDecorator,
|
||||
],
|
||||
args: {
|
||||
fieldMetadata: mockedCompaniesMetadata.node.fields.edges.find(
|
||||
({ node }) => node.type === FieldMetadataType.Text,
|
||||
)?.node as Field,
|
||||
objectMetadataId: mockedCompaniesMetadata.node.id,
|
||||
},
|
||||
parameters: {
|
||||
records: [
|
||||
{
|
||||
id: `${mockedCompaniesMetadata.node.id}-field-form`,
|
||||
domainName: 'Test',
|
||||
idealCustomerProfile: true,
|
||||
annualRecurringRevenue: {
|
||||
amountMicros: 1000000,
|
||||
currency: 'USD',
|
||||
},
|
||||
updatedAt: '2021-08-05T14:00:00.000Z',
|
||||
linkedinLink: {
|
||||
label: 'LinkedIn',
|
||||
url: 'https://linkedin.com',
|
||||
},
|
||||
employees: 100,
|
||||
},
|
||||
],
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsObjectFieldPreview>;
|
||||
|
||||
export const Text: Story = {};
|
||||
|
||||
export const Boolean: Story = {
|
||||
args: {
|
||||
fieldMetadata: mockedCompaniesMetadata.node.fields.edges.find(
|
||||
({ node }) => node.type === FieldMetadataType.Boolean,
|
||||
)?.node as Field,
|
||||
},
|
||||
};
|
||||
|
||||
export const Currency: Story = {
|
||||
args: {
|
||||
fieldMetadata: mockedCompaniesMetadata.node.fields.edges.find(
|
||||
({ node }) => node.type === FieldMetadataType.Currency,
|
||||
)?.node as Field,
|
||||
},
|
||||
};
|
||||
|
||||
export const Date: Story = {
|
||||
args: {
|
||||
fieldMetadata: mockedCompaniesMetadata.node.fields.edges.find(
|
||||
({ node }) => node.type === FieldMetadataType.DateTime,
|
||||
)?.node as Field,
|
||||
},
|
||||
};
|
||||
|
||||
export const Link: Story = {
|
||||
decorators: [MemoryRouterDecorator],
|
||||
args: {
|
||||
fieldMetadata: mockedCompaniesMetadata.node.fields.edges.find(
|
||||
({ node }) => node.type === FieldMetadataType.Link,
|
||||
)?.node as Field,
|
||||
},
|
||||
};
|
||||
|
||||
export const Number: Story = {
|
||||
args: {
|
||||
fieldMetadata: mockedCompaniesMetadata.node.fields.edges.find(
|
||||
({ node }) => node.type === FieldMetadataType.Number,
|
||||
)?.node as Field,
|
||||
},
|
||||
};
|
||||
|
||||
export const Rating: Story = {
|
||||
args: {
|
||||
fieldMetadata: {
|
||||
icon: 'IconHandClick',
|
||||
label: 'Engagement',
|
||||
type: FieldMetadataType.Rating,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Relation: Story = {
|
||||
decorators: [MemoryRouterDecorator],
|
||||
args: {
|
||||
fieldMetadata: mockedPeopleMetadata.node.fields.edges.find(
|
||||
({ node }) => node.type === FieldMetadataType.Relation,
|
||||
)?.node as Field,
|
||||
objectMetadataId: mockedPeopleMetadata.node.id,
|
||||
relationObjectMetadataId: mockedCompaniesMetadata.node.id,
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomObject: Story = {
|
||||
args: {
|
||||
fieldMetadata: mockedCompaniesMetadata.node.fields.edges.find(
|
||||
({ node }) => node.isCustom,
|
||||
)?.node as Field,
|
||||
objectMetadataId: mockedCompaniesMetadata.node.id,
|
||||
},
|
||||
};
|
||||
@ -1,132 +0,0 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/test';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import {
|
||||
mockedCompaniesMetadata,
|
||||
mockedPeopleMetadata,
|
||||
} from '~/testing/mock-data/metadata';
|
||||
|
||||
import { fieldMetadataFormDefaultValues } from '../../hooks/useFieldMetadataForm';
|
||||
import {
|
||||
SettingsObjectFieldTypeSelectSection,
|
||||
SettingsObjectFieldTypeSelectSectionFormValues,
|
||||
} from '../SettingsObjectFieldTypeSelectSection';
|
||||
|
||||
const fieldMetadata = mockedCompaniesMetadata.node.fields.edges.find(
|
||||
({ node }) => node.type === FieldMetadataType.Text,
|
||||
)!.node;
|
||||
const { id: _id, ...fieldMetadataWithoutId } = fieldMetadata;
|
||||
|
||||
const meta: Meta<typeof SettingsObjectFieldTypeSelectSection> = {
|
||||
title: 'Modules/Settings/DataModel/SettingsObjectFieldTypeSelectSection',
|
||||
component: SettingsObjectFieldTypeSelectSection,
|
||||
decorators: [
|
||||
ComponentDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
SnackBarDecorator,
|
||||
],
|
||||
args: {
|
||||
fieldMetadata: fieldMetadataWithoutId,
|
||||
objectMetadataId: mockedCompaniesMetadata.node.id,
|
||||
onChange: fn(),
|
||||
values: fieldMetadataFormDefaultValues,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 512 },
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsObjectFieldTypeSelectSection>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
fieldMetadata,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithOpenSelect: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const inputField = await canvas.findByText('Text');
|
||||
|
||||
await userEvent.click(inputField);
|
||||
|
||||
const input = await canvas.findByText('Unique ID');
|
||||
await userEvent.click(input);
|
||||
|
||||
await userEvent.click(inputField);
|
||||
},
|
||||
};
|
||||
|
||||
const relationFieldMetadata = mockedPeopleMetadata.node.fields.edges.find(
|
||||
({ node }) => node.type === FieldMetadataType.Relation,
|
||||
)!.node;
|
||||
|
||||
export const WithRelationForm: Story = {
|
||||
decorators: [MemoryRouterDecorator],
|
||||
args: {
|
||||
fieldMetadata: mockedCompaniesMetadata.node.fields.edges.find(
|
||||
({ node }) => node.type === FieldMetadataType.Relation,
|
||||
)?.node,
|
||||
relationFieldMetadata,
|
||||
values: {
|
||||
...fieldMetadataFormDefaultValues,
|
||||
type: FieldMetadataType.Relation,
|
||||
relation: {
|
||||
field: relationFieldMetadata,
|
||||
objectMetadataId: mockedPeopleMetadata.node.id,
|
||||
type: RelationMetadataType.OneToMany,
|
||||
},
|
||||
} as unknown as SettingsObjectFieldTypeSelectSectionFormValues,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithSelectForm: Story = {
|
||||
args: {
|
||||
fieldMetadata: { label: 'Industry', icon: 'IconBuildingFactory2' },
|
||||
values: {
|
||||
...fieldMetadataFormDefaultValues,
|
||||
type: FieldMetadataType.Select,
|
||||
select: [
|
||||
{
|
||||
color: 'pink',
|
||||
isDefault: true,
|
||||
label: '💊 Health',
|
||||
value: 'HEALTH',
|
||||
},
|
||||
{
|
||||
color: 'purple',
|
||||
label: '🏭 Industry',
|
||||
value: 'INDUSTRY',
|
||||
},
|
||||
{ color: 'sky', label: '🤖 SaaS', value: 'SAAS' },
|
||||
{
|
||||
color: 'turquoise',
|
||||
label: '🌿 Green tech',
|
||||
value: 'GREEN_TECH',
|
||||
},
|
||||
{
|
||||
color: 'yellow',
|
||||
label: '🚲 Mobility',
|
||||
value: 'MOBILITY',
|
||||
},
|
||||
{ color: 'green', label: '🌏 NGO', value: 'NGO' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -91,5 +91,9 @@ export const SETTINGS_FIELD_METADATA_TYPES: Partial<
|
||||
Icon: IconTwentyStar,
|
||||
defaultValue: '3',
|
||||
},
|
||||
[FieldMetadataType.FullName]: { label: 'Full Name', Icon: IconUser },
|
||||
[FieldMetadataType.FullName]: {
|
||||
label: 'Full Name',
|
||||
Icon: IconUser,
|
||||
defaultValue: { firstName: 'John', lastName: 'Doe' },
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,158 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import {
|
||||
SettingsObjectFieldCurrencyForm,
|
||||
SettingsObjectFieldCurrencyFormValues,
|
||||
} from '@/settings/data-model/components/SettingsObjectFieldCurrencyForm';
|
||||
import {
|
||||
SettingsObjectFieldRelationForm,
|
||||
SettingsObjectFieldRelationFormValues,
|
||||
} from '@/settings/data-model/components/SettingsObjectFieldRelationForm';
|
||||
import {
|
||||
SettingsObjectFieldSelectForm,
|
||||
SettingsObjectFieldSelectFormValues,
|
||||
} from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes';
|
||||
import {
|
||||
SettingsDataModelFieldPreviewCard,
|
||||
SettingsDataModelFieldPreviewCardProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export type SettingsDataModelFieldSettingsFormValues = {
|
||||
currency: SettingsObjectFieldCurrencyFormValues;
|
||||
relation: SettingsObjectFieldRelationFormValues;
|
||||
select: SettingsObjectFieldSelectFormValues;
|
||||
};
|
||||
|
||||
type SettingsDataModelFieldSettingsFormCardProps = {
|
||||
disableCurrencyForm?: boolean;
|
||||
onChange: (values: Partial<SettingsDataModelFieldSettingsFormValues>) => void;
|
||||
relationFieldMetadataItem?: Pick<
|
||||
FieldMetadataItem,
|
||||
'id' | 'isCustom' | 'name'
|
||||
>;
|
||||
values: SettingsDataModelFieldSettingsFormValues;
|
||||
} & Pick<
|
||||
SettingsDataModelFieldPreviewCardProps,
|
||||
'fieldMetadataItem' | 'objectMetadataItem'
|
||||
>;
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
display: grid;
|
||||
flex: 1 1 100%;
|
||||
`;
|
||||
|
||||
const StyledPreviewContent = styled.div`
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
const StyledRelationImage = styled.img<{ flip?: boolean }>`
|
||||
transform: ${({ flip }) => (flip ? 'scaleX(-1)' : 'none')};
|
||||
width: 54px;
|
||||
`;
|
||||
|
||||
const previewableTypes = [
|
||||
FieldMetadataType.Boolean,
|
||||
FieldMetadataType.Currency,
|
||||
FieldMetadataType.DateTime,
|
||||
FieldMetadataType.Select,
|
||||
FieldMetadataType.Link,
|
||||
FieldMetadataType.Number,
|
||||
FieldMetadataType.Rating,
|
||||
FieldMetadataType.Relation,
|
||||
FieldMetadataType.Text,
|
||||
];
|
||||
|
||||
export const SettingsDataModelFieldSettingsFormCard = ({
|
||||
disableCurrencyForm,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
onChange,
|
||||
relationFieldMetadataItem,
|
||||
values,
|
||||
}: SettingsDataModelFieldSettingsFormCardProps) => {
|
||||
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
|
||||
|
||||
if (!previewableTypes.includes(fieldMetadataItem.type)) return null;
|
||||
|
||||
const relationObjectMetadataItem = findObjectMetadataItemById(
|
||||
values.relation.objectMetadataId,
|
||||
);
|
||||
const relationTypeConfig = RELATION_TYPES[values.relation.type];
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
<StyledPreviewContent>
|
||||
<StyledFieldPreviewCard
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
shrink={fieldMetadataItem.type === FieldMetadataType.Relation}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
relationObjectMetadataItem={relationObjectMetadataItem}
|
||||
selectOptions={values.select}
|
||||
/>
|
||||
{fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
!!relationObjectMetadataItem && (
|
||||
<>
|
||||
<StyledRelationImage
|
||||
src={relationTypeConfig.imageSrc}
|
||||
flip={relationTypeConfig.isImageFlipped}
|
||||
alt={relationTypeConfig.label}
|
||||
/>
|
||||
<StyledFieldPreviewCard
|
||||
fieldMetadataItem={{
|
||||
icon: values.relation.field.icon,
|
||||
label: values.relation.field.label || 'Field name',
|
||||
type: FieldMetadataType.Relation,
|
||||
name: relationFieldMetadataItem?.name,
|
||||
id: relationFieldMetadataItem?.id,
|
||||
}}
|
||||
shrink
|
||||
objectMetadataItem={relationObjectMetadataItem}
|
||||
relationObjectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</StyledPreviewContent>
|
||||
}
|
||||
form={
|
||||
fieldMetadataItem.type === FieldMetadataType.Currency ? (
|
||||
<SettingsObjectFieldCurrencyForm
|
||||
disabled={disableCurrencyForm}
|
||||
values={values.currency}
|
||||
onChange={(nextCurrencyValues) =>
|
||||
onChange({
|
||||
currency: { ...values.currency, ...nextCurrencyValues },
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Relation ? (
|
||||
<SettingsObjectFieldRelationForm
|
||||
disableFieldEdition={
|
||||
relationFieldMetadataItem && !relationFieldMetadataItem.isCustom
|
||||
}
|
||||
disableRelationEdition={!!relationFieldMetadataItem}
|
||||
values={values.relation}
|
||||
onChange={(nextRelationValues) =>
|
||||
onChange({
|
||||
relation: { ...values.relation, ...nextRelationValues },
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Select ? (
|
||||
<SettingsObjectFieldSelectForm
|
||||
values={values.select}
|
||||
onChange={(nextSelectValues) =>
|
||||
onChange({ select: nextSelectValues })
|
||||
}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { SETTINGS_FIELD_METADATA_TYPES } from '@/settings/data-model/constants/SettingsFieldMetadataTypes';
|
||||
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
type SettingsDataModelFieldTypeSelectProps = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
excludedFieldTypes?: FieldMetadataType[];
|
||||
onChange?: ({ type }: { type: FieldMetadataType }) => void;
|
||||
value?: FieldMetadataType;
|
||||
};
|
||||
|
||||
export const SettingsDataModelFieldTypeSelect = ({
|
||||
className,
|
||||
disabled,
|
||||
excludedFieldTypes,
|
||||
onChange,
|
||||
value,
|
||||
}: SettingsDataModelFieldTypeSelectProps) => {
|
||||
const fieldTypeOptions = Object.entries(SETTINGS_FIELD_METADATA_TYPES)
|
||||
.filter(([key]) => !excludedFieldTypes?.includes(key as FieldMetadataType))
|
||||
.map<SelectOption<FieldMetadataType>>(([key, dataTypeConfig]) => ({
|
||||
Icon: dataTypeConfig.Icon,
|
||||
label: dataTypeConfig.label,
|
||||
value: key as FieldMetadataType,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
className={className}
|
||||
fullWidth
|
||||
disabled={disabled}
|
||||
dropdownId="object-field-type-select"
|
||||
value={value}
|
||||
onChange={(value) => onChange?.({ type: value })}
|
||||
options={fieldTypeOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,115 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { fn } from '@storybook/test';
|
||||
|
||||
import {
|
||||
mockedCompanyObjectMetadataItem,
|
||||
mockedPersonObjectMetadataItem,
|
||||
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
|
||||
import { fieldMetadataFormDefaultValues } from '@/settings/data-model/fields/forms/hooks/useFieldMetadataForm';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
|
||||
import { SettingsDataModelFieldSettingsFormCard } from '../SettingsDataModelFieldSettingsFormCard';
|
||||
|
||||
const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ type }) => type === FieldMetadataType.Text,
|
||||
)!;
|
||||
|
||||
const defaultValues = {
|
||||
currency: fieldMetadataFormDefaultValues.currency,
|
||||
relation: fieldMetadataFormDefaultValues.relation,
|
||||
select: fieldMetadataFormDefaultValues.select,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof SettingsDataModelFieldSettingsFormCard> = {
|
||||
title:
|
||||
'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldSettingsFormCard',
|
||||
component: SettingsDataModelFieldSettingsFormCard,
|
||||
decorators: [
|
||||
ComponentDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
SnackBarDecorator,
|
||||
],
|
||||
args: {
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
onChange: fn(),
|
||||
values: defaultValues,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 512 },
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsDataModelFieldSettingsFormCard>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
const relationFieldMetadataItem = mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'company',
|
||||
)!;
|
||||
|
||||
export const WithRelationForm: Story = {
|
||||
decorators: [MemoryRouterDecorator],
|
||||
args: {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'people',
|
||||
),
|
||||
relationFieldMetadataItem,
|
||||
values: {
|
||||
...defaultValues,
|
||||
relation: {
|
||||
field: relationFieldMetadataItem,
|
||||
objectMetadataId: mockedPersonObjectMetadataItem.id,
|
||||
type: RelationMetadataType.OneToMany,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithSelectForm: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: {
|
||||
label: 'Industry',
|
||||
icon: 'IconBuildingFactory2',
|
||||
type: FieldMetadataType.Select,
|
||||
},
|
||||
values: {
|
||||
...defaultValues,
|
||||
select: [
|
||||
{
|
||||
color: 'pink',
|
||||
isDefault: true,
|
||||
label: '💊 Health',
|
||||
value: 'HEALTH',
|
||||
},
|
||||
{
|
||||
color: 'purple',
|
||||
label: '🏭 Industry',
|
||||
value: 'INDUSTRY',
|
||||
},
|
||||
{ color: 'sky', label: '🤖 SaaS', value: 'SAAS' },
|
||||
{
|
||||
color: 'turquoise',
|
||||
label: '🌿 Green tech',
|
||||
value: 'GREEN_TECH',
|
||||
},
|
||||
{
|
||||
color: 'yellow',
|
||||
label: '🚲 Mobility',
|
||||
value: 'MOBILITY',
|
||||
},
|
||||
{ color: 'green', label: '🌏 NGO', value: 'NGO' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,67 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
|
||||
import { SettingsDataModelFieldTypeSelect } from '../SettingsDataModelFieldTypeSelect';
|
||||
|
||||
const meta: Meta<typeof SettingsDataModelFieldTypeSelect> = {
|
||||
title:
|
||||
'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldTypeSelect',
|
||||
component: SettingsDataModelFieldTypeSelect,
|
||||
decorators: [ComponentDecorator],
|
||||
args: {
|
||||
onChange: fn(),
|
||||
value: FieldMetadataType.Text,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 512 },
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsDataModelFieldTypeSelect>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithOpenSelect: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const inputField = await canvas.findByText('Text');
|
||||
|
||||
await userEvent.click(inputField);
|
||||
|
||||
const input = await canvas.findByText('Unique ID');
|
||||
await userEvent.click(input);
|
||||
|
||||
await userEvent.click(inputField);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithExcludedFieldTypes: Story = {
|
||||
args: {
|
||||
excludedFieldTypes: [FieldMetadataType.Uuid, FieldMetadataType.Numeric],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const inputField = await canvas.findByText('Text');
|
||||
|
||||
await userEvent.click(inputField);
|
||||
|
||||
await canvas.findByText('Number');
|
||||
|
||||
expect(canvas.queryByText('Unique ID')).toBeNull();
|
||||
expect(canvas.queryByText('Numeric')).toBeNull();
|
||||
},
|
||||
};
|
||||
@ -11,13 +11,14 @@ import {
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { SettingsObjectFieldTypeSelectSectionFormValues } from '../components/SettingsObjectFieldTypeSelectSection';
|
||||
import { SettingsDataModelFieldSettingsFormValues } from '../components/SettingsDataModelFieldSettingsFormCard';
|
||||
|
||||
type FormValues = {
|
||||
description?: string;
|
||||
icon: string;
|
||||
label: string;
|
||||
} & SettingsObjectFieldTypeSelectSectionFormValues;
|
||||
type: FieldMetadataType;
|
||||
} & SettingsDataModelFieldSettingsFormValues;
|
||||
|
||||
export const fieldMetadataFormDefaultValues: FormValues = {
|
||||
icon: 'IconUsers',
|
||||
@ -0,0 +1,125 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { parseFieldType } from '@/object-metadata/utils/parseFieldType';
|
||||
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { BooleanFieldInput } from '@/object-record/record-field/meta-types/input/components/BooleanFieldInput';
|
||||
import { RatingFieldInput } from '@/object-record/record-field/meta-types/input/components/RatingFieldInput';
|
||||
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect';
|
||||
import { SettingsDataModelSetRecordEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect';
|
||||
import { useFieldPreview } from '@/settings/data-model/fields/preview/hooks/useFieldPreview';
|
||||
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export type SettingsDataModelFieldPreviewProps = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> & {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
relationObjectMetadataItem?: ObjectMetadataItem;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
shrink?: boolean;
|
||||
withFieldLabel?: boolean;
|
||||
};
|
||||
|
||||
const StyledFieldPreview = styled.div<{ shrink?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
overflow: hidden;
|
||||
padding: 0
|
||||
${({ shrink, theme }) => (shrink ? theme.spacing(1) : theme.spacing(2))};
|
||||
white-space: nowrap;
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledFieldLabel = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const SettingsDataModelFieldPreview = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
selectOptions,
|
||||
shrink,
|
||||
withFieldLabel = true,
|
||||
}: SettingsDataModelFieldPreviewProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
const FieldIcon = getIcon(fieldMetadataItem.icon);
|
||||
|
||||
const { entityId, fieldName, fieldPreviewValue, isLabelIdentifier, record } =
|
||||
useFieldPreview({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
selectOptions,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{record ? (
|
||||
<SettingsDataModelSetRecordEffect record={record} />
|
||||
) : (
|
||||
<SettingsDataModelSetFieldValueEffect
|
||||
entityId={entityId}
|
||||
fieldName={fieldName}
|
||||
value={fieldPreviewValue}
|
||||
/>
|
||||
)}
|
||||
<StyledFieldPreview shrink={shrink}>
|
||||
{!!withFieldLabel && (
|
||||
<StyledFieldLabel>
|
||||
<FieldIcon
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
{fieldMetadataItem.label}:
|
||||
</StyledFieldLabel>
|
||||
)}
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
entityId,
|
||||
isLabelIdentifier,
|
||||
fieldDefinition: {
|
||||
type: parseFieldType(fieldMetadataItem.type),
|
||||
iconName: 'FieldIcon',
|
||||
fieldMetadataId: fieldMetadataItem.id || '',
|
||||
label: fieldMetadataItem.label,
|
||||
metadata: {
|
||||
fieldName,
|
||||
objectMetadataNameSingular: objectMetadataItem.nameSingular,
|
||||
relationObjectMetadataNameSingular:
|
||||
relationObjectMetadataItem?.nameSingular,
|
||||
options: selectOptions,
|
||||
},
|
||||
},
|
||||
hotkeyScope: 'field-preview',
|
||||
}}
|
||||
>
|
||||
{fieldMetadataItem.type === FieldMetadataType.Boolean ? (
|
||||
<BooleanFieldInput readonly />
|
||||
) : fieldMetadataItem.type === FieldMetadataType.Rating ? (
|
||||
<RatingFieldInput readonly />
|
||||
) : (
|
||||
<FieldDisplay />
|
||||
)}
|
||||
</FieldContext.Provider>
|
||||
</StyledFieldPreview>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,48 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import {
|
||||
SettingsDataModelFieldPreview,
|
||||
SettingsDataModelFieldPreviewProps,
|
||||
} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview';
|
||||
import { SettingsDataModelObjectSummary } from '@/settings/data-model/objects/SettingsDataModelObjectSummary';
|
||||
import { Card } from '@/ui/layout/card/components/Card';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
|
||||
export type SettingsDataModelFieldPreviewCardProps =
|
||||
SettingsDataModelFieldPreviewProps & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
`;
|
||||
|
||||
const StyledCardContent = styled(CardContent)`
|
||||
display: grid;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const SettingsDataModelFieldPreviewCard = ({
|
||||
className,
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
selectOptions,
|
||||
shrink,
|
||||
withFieldLabel = true,
|
||||
}: SettingsDataModelFieldPreviewCardProps) => (
|
||||
<StyledCard className={className} fullWidth>
|
||||
<StyledCardContent>
|
||||
<SettingsDataModelObjectSummary objectMetadataItem={objectMetadataItem} />
|
||||
<SettingsDataModelFieldPreview
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
relationObjectMetadataItem={relationObjectMetadataItem}
|
||||
selectOptions={selectOptions}
|
||||
shrink={shrink}
|
||||
withFieldLabel={withFieldLabel}
|
||||
/>
|
||||
</StyledCardContent>
|
||||
</StyledCard>
|
||||
);
|
||||
@ -3,17 +3,17 @@ import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
|
||||
type SettingsObjectFieldPreviewValueEffectProps = {
|
||||
type SettingsDataModelSetFieldValueEffectProps = {
|
||||
entityId: string;
|
||||
fieldName: string;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
export const SettingsObjectFieldPreviewValueEffect = ({
|
||||
export const SettingsDataModelSetFieldValueEffect = ({
|
||||
entityId,
|
||||
fieldName,
|
||||
value,
|
||||
}: SettingsObjectFieldPreviewValueEffectProps) => {
|
||||
}: SettingsDataModelSetFieldValueEffectProps) => {
|
||||
const setFieldValue = useSetRecoilState(
|
||||
recordStoreFamilySelector({
|
||||
recordId: entityId,
|
||||
@ -0,0 +1,20 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
type SettingsDataModelSetRecordEffectProps = {
|
||||
record: ObjectRecord;
|
||||
};
|
||||
|
||||
export const SettingsDataModelSetRecordEffect = ({
|
||||
record,
|
||||
}: SettingsDataModelSetRecordEffectProps) => {
|
||||
const { setRecords: setRecordsInStore } = useSetRecordInStore();
|
||||
|
||||
useEffect(() => {
|
||||
setRecordsInStore([record]);
|
||||
}, [record, setRecordsInStore]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -0,0 +1,111 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import {
|
||||
mockedCompanyObjectMetadataItem,
|
||||
mockedPersonObjectMetadataItem,
|
||||
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
|
||||
import { SettingsDataModelFieldPreviewCard } from '../SettingsDataModelFieldPreviewCard';
|
||||
|
||||
const meta: Meta<typeof SettingsDataModelFieldPreviewCard> = {
|
||||
title:
|
||||
'Modules/Settings/DataModel/Fields/Preview/SettingsDataModelFieldPreviewCard',
|
||||
component: SettingsDataModelFieldPreviewCard,
|
||||
decorators: [
|
||||
ComponentDecorator,
|
||||
ObjectMetadataItemsDecorator,
|
||||
SnackBarDecorator,
|
||||
],
|
||||
args: {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ type }) => type === FieldMetadataType.Text,
|
||||
),
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
},
|
||||
parameters: {
|
||||
container: { width: 480 },
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof SettingsDataModelFieldPreviewCard>;
|
||||
|
||||
export const Text: Story = {};
|
||||
|
||||
export const Boolean: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ type }) => type === FieldMetadataType.Boolean,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const Currency: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ type }) => type === FieldMetadataType.Currency,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const Date: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ type }) => type === FieldMetadataType.DateTime,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const Link: Story = {
|
||||
decorators: [MemoryRouterDecorator],
|
||||
args: {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ type }) => type === FieldMetadataType.Link,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const Number: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ type }) => type === FieldMetadataType.Number,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const Rating: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: {
|
||||
icon: 'IconHandClick',
|
||||
label: 'Engagement',
|
||||
type: FieldMetadataType.Rating,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Relation: Story = {
|
||||
decorators: [MemoryRouterDecorator],
|
||||
args: {
|
||||
fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'company',
|
||||
),
|
||||
objectMetadataItem: mockedPersonObjectMetadataItem,
|
||||
relationObjectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomObject: Story = {
|
||||
args: {
|
||||
fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ isCustom }) => isCustom,
|
||||
),
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,67 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { mockedCompanyObjectMetadataItem } from '@/object-record/record-field/__mocks__/fieldDefinitions';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
|
||||
import { useFieldPreview } from '../useFieldPreview';
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useFieldPreview', () => {
|
||||
it('returns default preview data if no records are found', () => {
|
||||
// Given
|
||||
const objectMetadataItem = mockedCompanyObjectMetadataItem;
|
||||
const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'linkedinLink',
|
||||
)!;
|
||||
|
||||
// When
|
||||
const { result } = renderHook(
|
||||
() => useFieldPreview({ fieldMetadataItem, objectMetadataItem }),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(result.current).toEqual({
|
||||
entityId: 'company-linkedinLink-preview-field-form',
|
||||
fieldName: 'linkedinLink',
|
||||
fieldPreviewValue: { label: '', url: 'www.twenty.com' },
|
||||
isLabelIdentifier: false,
|
||||
record: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns default preview data for a label identifier field if no records are found', () => {
|
||||
// Given
|
||||
const objectMetadataItem = mockedCompanyObjectMetadataItem;
|
||||
const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'name',
|
||||
)!;
|
||||
|
||||
// When
|
||||
const { result } = renderHook(
|
||||
() => useFieldPreview({ fieldMetadataItem, objectMetadataItem }),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
// Then
|
||||
expect(result.current).toEqual({
|
||||
entityId: 'company-name-preview-field-form',
|
||||
fieldName: 'name',
|
||||
fieldPreviewValue: 'Company',
|
||||
isLabelIdentifier: true,
|
||||
record: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,101 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { parseFieldType } from '@/object-metadata/utils/parseFieldType';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
|
||||
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { getFieldDefaultPreviewValue } from '@/settings/data-model/utils/getFieldDefaultPreviewValue';
|
||||
import { getFieldPreviewValueFromRecord } from '@/settings/data-model/utils/getFieldPreviewValueFromRecord';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
type UseFieldPreviewParams = {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'type'> & {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
relationObjectMetadataItem?: ObjectMetadataItem;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
};
|
||||
|
||||
export const useFieldPreview = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
selectOptions,
|
||||
}: UseFieldPreviewParams) => {
|
||||
const isLabelIdentifier =
|
||||
!!fieldMetadataItem.id &&
|
||||
!!fieldMetadataItem.name &&
|
||||
isLabelIdentifierField({
|
||||
fieldMetadataItem: {
|
||||
id: fieldMetadataItem.id,
|
||||
name: fieldMetadataItem.name,
|
||||
},
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const { records } = useFindManyRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
limit: 1,
|
||||
skip: !fieldMetadataItem.name,
|
||||
});
|
||||
const [firstRecord] = records;
|
||||
|
||||
const fieldPreviewValueFromFirstRecord =
|
||||
firstRecord && fieldMetadataItem.name
|
||||
? getFieldPreviewValueFromRecord({
|
||||
record: firstRecord,
|
||||
fieldMetadataItem: {
|
||||
name: fieldMetadataItem.name,
|
||||
type: fieldMetadataItem.type,
|
||||
},
|
||||
selectOptions,
|
||||
})
|
||||
: null;
|
||||
|
||||
const isValueFromFirstRecord =
|
||||
firstRecord &&
|
||||
!isFieldValueEmpty({
|
||||
fieldDefinition: { type: parseFieldType(fieldMetadataItem.type) },
|
||||
fieldValue: fieldPreviewValueFromFirstRecord,
|
||||
});
|
||||
|
||||
const { records: relationRecords } = useFindManyRecords({
|
||||
objectNameSingular:
|
||||
relationObjectMetadataItem?.nameSingular ||
|
||||
CoreObjectNameSingular.Company,
|
||||
limit: 1,
|
||||
skip:
|
||||
!relationObjectMetadataItem ||
|
||||
fieldMetadataItem.type !== FieldMetadataType.Relation ||
|
||||
isValueFromFirstRecord,
|
||||
});
|
||||
const [firstRelationRecord] = relationRecords;
|
||||
|
||||
const fieldPreviewValue = isValueFromFirstRecord
|
||||
? fieldPreviewValueFromFirstRecord
|
||||
: firstRelationRecord ??
|
||||
getFieldDefaultPreviewValue({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
selectOptions,
|
||||
});
|
||||
|
||||
const fieldName =
|
||||
fieldMetadataItem.name || `${fieldMetadataItem.type}-new-field`;
|
||||
const entityId = isValueFromFirstRecord
|
||||
? firstRecord.id
|
||||
: `${objectMetadataItem.nameSingular}-${fieldMetadataItem.name}-preview-field-form`;
|
||||
|
||||
return {
|
||||
entityId,
|
||||
fieldName,
|
||||
fieldPreviewValue,
|
||||
isLabelIdentifier,
|
||||
record: isValueFromFirstRecord ? firstRecord : null,
|
||||
};
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
|
||||
import { useFieldPreview } from '../useFieldPreview';
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
||||
|
||||
describe('useFieldPreview', () => {
|
||||
it('returns default values', () => {
|
||||
const objectMetadataItem = mockObjectMetadataItems[1];
|
||||
const fieldMetadata = objectMetadataItem.fields[0];
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setMetadataItems = useSetRecoilState(objectMetadataItemsState);
|
||||
setMetadataItems(mockObjectMetadataItems);
|
||||
|
||||
return useFieldPreview({
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
fieldMetadata,
|
||||
});
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.entityId).toBe(`${objectMetadataItem.id}-field-form`);
|
||||
expect(result.current.FieldIcon).toBeDefined();
|
||||
expect(result.current.fieldName).toBe(fieldMetadata.name);
|
||||
expect(result.current.ObjectIcon).toBeDefined();
|
||||
expect(result.current.fieldName).toBe(fieldMetadata.name);
|
||||
expect(result.current.objectMetadataItem?.id).toBe(objectMetadataItem.id);
|
||||
expect(result.current.relationObjectMetadataItem).toBeUndefined();
|
||||
expect(result.current.value).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -1,74 +0,0 @@
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { SETTINGS_FIELD_METADATA_TYPES } from '@/settings/data-model/constants/SettingsFieldMetadataTypes';
|
||||
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
|
||||
import { Field, FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
import { SettingsObjectFieldSelectFormOption } from '../types/SettingsObjectFieldSelectFormOption';
|
||||
|
||||
import { useFieldPreviewValue } from './useFieldPreviewValue';
|
||||
import { useRelationFieldPreviewValue } from './useRelationFieldPreviewValue';
|
||||
|
||||
export const useFieldPreview = ({
|
||||
fieldMetadata,
|
||||
objectMetadataId,
|
||||
relationObjectMetadataId,
|
||||
selectOptions,
|
||||
}: {
|
||||
fieldMetadata: Pick<Field, 'icon' | 'label' | 'type'> & { id?: string };
|
||||
objectMetadataId: string;
|
||||
relationObjectMetadataId?: string;
|
||||
selectOptions?: SettingsObjectFieldSelectFormOption[];
|
||||
}) => {
|
||||
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
|
||||
const objectMetadataItem = findObjectMetadataItemById(objectMetadataId);
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
const ObjectIcon = getIcon(objectMetadataItem?.icon);
|
||||
const FieldIcon = getIcon(fieldMetadata.icon);
|
||||
|
||||
const fieldName = fieldMetadata.id
|
||||
? objectMetadataItem?.fields.find(({ id }) => id === fieldMetadata.id)?.name
|
||||
: undefined;
|
||||
|
||||
const { value: firstRecordFieldValue } = useFieldPreviewValue({
|
||||
fieldName: fieldName || '',
|
||||
objectNamePlural: objectMetadataItem?.namePlural ?? '',
|
||||
skip:
|
||||
!fieldName ||
|
||||
!objectMetadataItem ||
|
||||
fieldMetadata.type === FieldMetadataType.Relation,
|
||||
});
|
||||
|
||||
const { relationObjectMetadataItem, value: relationValue } =
|
||||
useRelationFieldPreviewValue({
|
||||
relationObjectMetadataId,
|
||||
skip: fieldMetadata.type !== FieldMetadataType.Relation,
|
||||
});
|
||||
|
||||
const settingsFieldMetadataType =
|
||||
SETTINGS_FIELD_METADATA_TYPES[fieldMetadata.type];
|
||||
|
||||
const defaultSelectValue = selectOptions?.[0];
|
||||
const selectValue =
|
||||
fieldMetadata.type === FieldMetadataType.Select &&
|
||||
typeof firstRecordFieldValue === 'string'
|
||||
? selectOptions?.find(
|
||||
(selectOption) => selectOption.value === firstRecordFieldValue,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
entityId: `${objectMetadataId}-field-form`,
|
||||
FieldIcon,
|
||||
fieldName: fieldName || `${fieldMetadata.type}-new-field`,
|
||||
ObjectIcon,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
value:
|
||||
fieldMetadata.type === FieldMetadataType.Relation
|
||||
? relationValue
|
||||
: fieldMetadata.type === FieldMetadataType.Select
|
||||
? selectValue || defaultSelectValue
|
||||
: firstRecordFieldValue || settingsFieldMetadataType?.defaultValue,
|
||||
};
|
||||
};
|
||||
@ -1,30 +0,0 @@
|
||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
|
||||
export const useFieldPreviewValue = ({
|
||||
fieldName,
|
||||
objectNamePlural,
|
||||
skip,
|
||||
}: {
|
||||
fieldName: string;
|
||||
objectNamePlural: string;
|
||||
skip?: boolean;
|
||||
}) => {
|
||||
const { objectNameSingular } = useObjectNameSingularFromPlural({
|
||||
objectNamePlural,
|
||||
});
|
||||
|
||||
const { records } = useFindManyRecords({
|
||||
objectNameSingular,
|
||||
skip,
|
||||
});
|
||||
|
||||
const firstRecordWithValue = records.find(
|
||||
(record) => assertNotNull(record[fieldName]) && record[fieldName] !== '',
|
||||
);
|
||||
|
||||
return {
|
||||
value: firstRecordWithValue?.[fieldName],
|
||||
};
|
||||
};
|
||||
@ -1,35 +0,0 @@
|
||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
export const useRelationFieldPreviewValue = ({
|
||||
relationObjectMetadataId,
|
||||
skip,
|
||||
}: {
|
||||
relationObjectMetadataId?: string;
|
||||
skip?: boolean;
|
||||
}) => {
|
||||
const { findObjectMetadataItemById } = useObjectMetadataItemForSettings();
|
||||
|
||||
// TODO: make this impossible to be undefined
|
||||
const relationObjectMetadataItem = relationObjectMetadataId
|
||||
? findObjectMetadataItemById(relationObjectMetadataId)
|
||||
: undefined;
|
||||
|
||||
const { records: relationObjects } = useFindManyRecords({
|
||||
objectNameSingular:
|
||||
relationObjectMetadataItem?.nameSingular ??
|
||||
CoreObjectNameSingular.Company, // TODO fix this hack
|
||||
skip: skip || !relationObjectMetadataItem,
|
||||
});
|
||||
|
||||
const label = relationObjectMetadataItem?.labelSingular ?? '';
|
||||
|
||||
return {
|
||||
relationObjectMetadataItem,
|
||||
value: relationObjects?.[0] ?? {
|
||||
company: { name: label }, // Temporary mock for opportunities, this needs to be replaced once labelIdentifiers are implemented
|
||||
name: label,
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -1,9 +1,9 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { SettingsDataModelIsCustomTag } from '@/settings/data-model/objects/SettingsDataModelIsCustomTag';
|
||||
import { IconArchive, IconDotsVertical, IconPencil } from '@/ui/display/icon';
|
||||
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
|
||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { Card } from '@/ui/layout/card/components/Card';
|
||||
@ -38,7 +38,7 @@ const StyledName = styled.div`
|
||||
margin-right: auto;
|
||||
`;
|
||||
|
||||
const StyledTag = styled(Tag)`
|
||||
const StyledIsCustomTag = styled(SettingsDataModelIsCustomTag)`
|
||||
box-sizing: border-box;
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
@ -77,11 +77,7 @@ export const SettingsAboutSection = ({
|
||||
{!!Icon && <Icon size={theme.icon.size.md} />}
|
||||
{name}
|
||||
</StyledName>
|
||||
{isCustom ? (
|
||||
<StyledTag color="orange" text="Custom" weight="medium" />
|
||||
) : (
|
||||
<StyledTag color="blue" text="Standard" weight="medium" />
|
||||
)}
|
||||
<StyledIsCustomTag isCustom={isCustom} />
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
|
||||
@ -4,8 +4,8 @@ import styled from '@emotion/styled';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { SettingsDataModelIsCustomTag } from '@/settings/data-model/objects/SettingsDataModelIsCustomTag';
|
||||
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
|
||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
|
||||
@ -52,11 +52,7 @@ export const SettingsObjectItemTableRow = ({
|
||||
{objectItem.labelPlural}
|
||||
</StyledNameTableCell>
|
||||
<TableCell>
|
||||
{objectItem.isCustom ? (
|
||||
<Tag color="orange" text="Custom" weight="regular" />
|
||||
) : (
|
||||
<Tag color="blue" text="Standard" weight="regular" />
|
||||
)}
|
||||
<SettingsDataModelIsCustomTag isCustom={objectItem.isCustom} />
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{objectItem.fields.filter((field) => !field.isSystem).length}
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import { Tag } from '@/ui/display/tag/components/Tag';
|
||||
|
||||
type SettingsDataModelIsCustomTagProps = {
|
||||
className?: string;
|
||||
isCustom?: boolean;
|
||||
};
|
||||
|
||||
export const SettingsDataModelIsCustomTag = ({
|
||||
className,
|
||||
isCustom,
|
||||
}: SettingsDataModelIsCustomTagProps) => (
|
||||
<Tag
|
||||
className={className}
|
||||
color={isCustom ? 'orange' : 'blue'}
|
||||
text={isCustom ? 'Custom' : 'Standard'}
|
||||
weight="medium"
|
||||
/>
|
||||
);
|
||||
@ -0,0 +1,48 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { SettingsDataModelIsCustomTag } from '@/settings/data-model/objects/SettingsDataModelIsCustomTag';
|
||||
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
|
||||
|
||||
export type SettingsDataModelObjectSummaryProps = {
|
||||
className?: string;
|
||||
objectMetadataItem: Pick<
|
||||
ObjectMetadataItem,
|
||||
'icon' | 'isCustom' | 'labelPlural'
|
||||
>;
|
||||
};
|
||||
|
||||
const StyledObjectSummary = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledObjectName = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const SettingsDataModelObjectSummary = ({
|
||||
className,
|
||||
objectMetadataItem,
|
||||
}: SettingsDataModelObjectSummaryProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
const ObjectIcon = getIcon(objectMetadataItem.icon);
|
||||
|
||||
return (
|
||||
<StyledObjectSummary className={className}>
|
||||
<StyledObjectName>
|
||||
<ObjectIcon size={theme.icon.size.sm} stroke={theme.icon.stroke.md} />
|
||||
{objectMetadataItem.labelPlural}
|
||||
</StyledObjectName>
|
||||
<SettingsDataModelIsCustomTag isCustom={objectMetadataItem.isCustom} />
|
||||
</StyledObjectSummary>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
|
||||
import { SettingsDataModelFieldPreviewCard } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
|
||||
import { SettingsDataModelObjectSummary } from '@/settings/data-model/objects/SettingsDataModelObjectSummary';
|
||||
import { Card } from '@/ui/layout/card/components/Card';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
|
||||
type SettingsDataModelObjectSettingsFormCardProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
|
||||
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledObjectSummaryCard = styled(Card)`
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
max-width: 480px;
|
||||
`;
|
||||
|
||||
const StyledObjectSummaryCardContent = styled(CardContent)`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const SettingsDataModelObjectSettingsFormCard = ({
|
||||
objectMetadataItem,
|
||||
}: SettingsDataModelObjectSettingsFormCardProps) => {
|
||||
const labelIdentifierFieldMetadataItem =
|
||||
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
|
||||
|
||||
return (
|
||||
<SettingsDataModelPreviewFormCard
|
||||
preview={
|
||||
labelIdentifierFieldMetadataItem ? (
|
||||
<StyledFieldPreviewCard
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
fieldMetadataItem={labelIdentifierFieldMetadataItem}
|
||||
withFieldLabel={false}
|
||||
/>
|
||||
) : (
|
||||
<StyledObjectSummaryCard>
|
||||
<StyledObjectSummaryCardContent>
|
||||
<SettingsDataModelObjectSummary
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
</StyledObjectSummaryCardContent>
|
||||
</StyledObjectSummaryCard>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,203 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
mockedCompanyObjectMetadataItem,
|
||||
mockedPersonObjectMetadataItem,
|
||||
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
|
||||
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
|
||||
import { getFieldDefaultPreviewValue } from '../getFieldDefaultPreviewValue';
|
||||
|
||||
describe('getFieldDefaultPreviewValue', () => {
|
||||
describe('SELECT field', () => {
|
||||
it('returns the default select option', () => {
|
||||
// Given
|
||||
const objectMetadataItem = mockedCompanyObjectMetadataItem;
|
||||
const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'industry',
|
||||
)!;
|
||||
const selectOptions: SettingsObjectFieldSelectFormValues = [
|
||||
{
|
||||
color: 'purple',
|
||||
label: '🏭 Industry',
|
||||
value: 'INDUSTRY',
|
||||
},
|
||||
{
|
||||
color: 'pink',
|
||||
isDefault: true,
|
||||
label: '💊 Health',
|
||||
value: 'HEALTH',
|
||||
},
|
||||
];
|
||||
|
||||
// When
|
||||
const result = getFieldDefaultPreviewValue({
|
||||
objectMetadataItem,
|
||||
fieldMetadataItem,
|
||||
selectOptions,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toEqual(selectOptions[1]);
|
||||
});
|
||||
|
||||
it('returns the first select option if no default option was found', () => {
|
||||
// Given
|
||||
const objectMetadataItem = mockedCompanyObjectMetadataItem;
|
||||
const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'industry',
|
||||
)!;
|
||||
const selectOptions = [
|
||||
{
|
||||
color: 'purple' as const,
|
||||
label: '🏭 Industry',
|
||||
value: 'INDUSTRY',
|
||||
},
|
||||
{
|
||||
color: 'pink' as const,
|
||||
label: '💊 Health',
|
||||
value: 'HEALTH',
|
||||
},
|
||||
];
|
||||
|
||||
// When
|
||||
const result = getFieldDefaultPreviewValue({
|
||||
objectMetadataItem,
|
||||
fieldMetadataItem,
|
||||
selectOptions,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toEqual(selectOptions[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RELATION field', () => {
|
||||
it('returns a record with a default label identifier (if relation label identifier type !== TEXT)', () => {
|
||||
// Given
|
||||
const objectMetadataItem = mockedCompanyObjectMetadataItem;
|
||||
const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'people',
|
||||
)!;
|
||||
const relationObjectMetadataItem = mockedPersonObjectMetadataItem;
|
||||
|
||||
// When
|
||||
const result = getFieldDefaultPreviewValue({
|
||||
objectMetadataItem,
|
||||
fieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({
|
||||
name: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a record with the relation object label singular as label identifier (if relation label identifier type === TEXT)', () => {
|
||||
// Given
|
||||
const objectMetadataItem = mockedPersonObjectMetadataItem;
|
||||
const fieldMetadataItem = mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'company',
|
||||
)!;
|
||||
const relationObjectMetadataItem = mockedCompanyObjectMetadataItem;
|
||||
|
||||
// When
|
||||
const result = getFieldDefaultPreviewValue({
|
||||
objectMetadataItem,
|
||||
fieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({
|
||||
name: 'Company',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null if the relation object does not have a label identifier field', () => {
|
||||
// Given
|
||||
const objectMetadataItem = mockedPersonObjectMetadataItem;
|
||||
const fieldMetadataItem = mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'company',
|
||||
)!;
|
||||
const relationObjectMetadataItem: ObjectMetadataItem = {
|
||||
...mockedCompanyObjectMetadataItem,
|
||||
labelIdentifierFieldMetadataId: null,
|
||||
fields: mockedCompanyObjectMetadataItem.fields.filter(
|
||||
({ name }) => name !== 'name',
|
||||
),
|
||||
};
|
||||
|
||||
// When
|
||||
const result = getFieldDefaultPreviewValue({
|
||||
objectMetadataItem,
|
||||
fieldMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Other fields', () => {
|
||||
it('returns the object singular name as default value for the label identifier field (type TEXT)', () => {
|
||||
// Given
|
||||
const objectMetadataItem = mockedCompanyObjectMetadataItem;
|
||||
const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'name',
|
||||
)!;
|
||||
|
||||
// When
|
||||
const result = getFieldDefaultPreviewValue({
|
||||
objectMetadataItem,
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBe('Company');
|
||||
});
|
||||
|
||||
it('returns a default value for the label identifier field (type FULL_NAME)', () => {
|
||||
// Given
|
||||
const objectMetadataItem = mockedPersonObjectMetadataItem;
|
||||
const fieldMetadataItem = mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'name',
|
||||
)!;
|
||||
|
||||
// When
|
||||
const result = getFieldDefaultPreviewValue({
|
||||
objectMetadataItem,
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a default value for other field types', () => {
|
||||
// Given
|
||||
const objectMetadataItem = mockedCompanyObjectMetadataItem;
|
||||
const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'domainName',
|
||||
)!;
|
||||
|
||||
// When
|
||||
const result = getFieldDefaultPreviewValue({
|
||||
objectMetadataItem,
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBe(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,149 @@
|
||||
import {
|
||||
mockedCompanyObjectMetadataItem,
|
||||
mockedPersonObjectMetadataItem,
|
||||
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
|
||||
import { getFieldPreviewValueFromRecord } from '../getFieldPreviewValueFromRecord';
|
||||
|
||||
describe('getFieldPreviewValueFromRecord', () => {
|
||||
describe('SELECT field', () => {
|
||||
it('returns the select option corresponding to the record field value', () => {
|
||||
// Given
|
||||
const record: ObjectRecord = { id: '', industry: 'GREEN_TECH' };
|
||||
const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'industry',
|
||||
)!;
|
||||
const selectOptions: SettingsObjectFieldSelectFormValues = [
|
||||
{
|
||||
color: 'purple',
|
||||
label: '🏭 Industry',
|
||||
value: 'INDUSTRY',
|
||||
},
|
||||
{
|
||||
color: 'pink',
|
||||
isDefault: true,
|
||||
label: '💊 Health',
|
||||
value: 'HEALTH',
|
||||
},
|
||||
{
|
||||
color: 'turquoise',
|
||||
label: '🌿 Green tech',
|
||||
value: 'GREEN_TECH',
|
||||
},
|
||||
];
|
||||
|
||||
// When
|
||||
const result = getFieldPreviewValueFromRecord({
|
||||
record,
|
||||
fieldMetadataItem,
|
||||
selectOptions,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toEqual(selectOptions[2]);
|
||||
});
|
||||
|
||||
it('returns undefined if the select option was not found', () => {
|
||||
// Given
|
||||
const record: ObjectRecord = { id: '', industry: 'MOBILITY' };
|
||||
const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'industry',
|
||||
)!;
|
||||
const selectOptions: SettingsObjectFieldSelectFormValues = [
|
||||
{
|
||||
color: 'purple',
|
||||
label: '🏭 Industry',
|
||||
value: 'INDUSTRY',
|
||||
},
|
||||
{
|
||||
color: 'pink',
|
||||
isDefault: true,
|
||||
label: '💊 Health',
|
||||
value: 'HEALTH',
|
||||
},
|
||||
{
|
||||
color: 'turquoise',
|
||||
label: '🌿 Green tech',
|
||||
value: 'GREEN_TECH',
|
||||
},
|
||||
];
|
||||
|
||||
// When
|
||||
const result = getFieldPreviewValueFromRecord({
|
||||
record,
|
||||
fieldMetadataItem,
|
||||
selectOptions,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RELATION field', () => {
|
||||
it('returns the first relation record from a list of edges ("to many" relation)', () => {
|
||||
// Given
|
||||
const firstRelationRecord = {
|
||||
id: '1',
|
||||
name: { firstName: 'Jane', lastName: 'Doe' },
|
||||
};
|
||||
const record: ObjectRecord = {
|
||||
id: '',
|
||||
people: {
|
||||
edges: [{ node: firstRelationRecord }, { node: { id: '2' } }],
|
||||
},
|
||||
};
|
||||
const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'people',
|
||||
)!;
|
||||
|
||||
// When
|
||||
const result = getFieldPreviewValueFromRecord({
|
||||
record,
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toEqual(firstRelationRecord);
|
||||
});
|
||||
|
||||
it('returns the record field value ("to one" relation)', () => {
|
||||
// Given
|
||||
const relationRecord = { id: '20', name: 'Twenty' };
|
||||
const record = { id: '', company: relationRecord };
|
||||
const fieldMetadataItem = mockedPersonObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'company',
|
||||
)!;
|
||||
|
||||
// When
|
||||
const result = getFieldPreviewValueFromRecord({
|
||||
record,
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toEqual(relationRecord);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Other fields', () => {
|
||||
it('returns the record field value', () => {
|
||||
// Given
|
||||
const record = { id: '', name: 'Twenty' };
|
||||
const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find(
|
||||
({ name }) => name === 'name',
|
||||
)!;
|
||||
|
||||
// When
|
||||
const result = getFieldPreviewValueFromRecord({
|
||||
record,
|
||||
fieldMetadataItem,
|
||||
});
|
||||
|
||||
// Then
|
||||
expect(result).toEqual(record.name);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,68 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { SETTINGS_FIELD_METADATA_TYPES } from '@/settings/data-model/constants/SettingsFieldMetadataTypes';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const getFieldDefaultPreviewValue = ({
|
||||
fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
relationObjectMetadataItem,
|
||||
selectOptions,
|
||||
}: {
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'type'> & {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
relationObjectMetadataItem?: ObjectMetadataItem;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
}) => {
|
||||
// Select field
|
||||
if (fieldMetadataItem.type === FieldMetadataType.Select && selectOptions) {
|
||||
return selectOptions.find(({ isDefault }) => isDefault) || selectOptions[0];
|
||||
}
|
||||
|
||||
// Relation field
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
relationObjectMetadataItem
|
||||
) {
|
||||
const relationLabelIdentifierFieldMetadataItem =
|
||||
getLabelIdentifierFieldMetadataItem(relationObjectMetadataItem);
|
||||
|
||||
if (!relationLabelIdentifierFieldMetadataItem) return null;
|
||||
|
||||
const defaultRelationLabelIdentifierFieldValue =
|
||||
relationLabelIdentifierFieldMetadataItem.type === FieldMetadataType.Text
|
||||
? relationObjectMetadataItem.labelSingular
|
||||
: SETTINGS_FIELD_METADATA_TYPES[
|
||||
relationLabelIdentifierFieldMetadataItem.type
|
||||
]?.defaultValue;
|
||||
|
||||
const defaultRelationRecord = {
|
||||
[relationLabelIdentifierFieldMetadataItem.name]:
|
||||
defaultRelationLabelIdentifierFieldValue,
|
||||
};
|
||||
|
||||
return defaultRelationRecord;
|
||||
}
|
||||
|
||||
const isLabelIdentifier =
|
||||
!!fieldMetadataItem.id &&
|
||||
!!fieldMetadataItem.name &&
|
||||
isLabelIdentifierField({
|
||||
fieldMetadataItem: {
|
||||
id: fieldMetadataItem.id,
|
||||
name: fieldMetadataItem.name,
|
||||
},
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
// Other fields
|
||||
return isLabelIdentifier && fieldMetadataItem.type === FieldMetadataType.Text
|
||||
? objectMetadataItem.labelSingular
|
||||
: SETTINGS_FIELD_METADATA_TYPES[fieldMetadataItem.type]?.defaultValue;
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { SettingsObjectFieldSelectFormValues } from '@/settings/data-model/components/SettingsObjectFieldSelectForm';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const getFieldPreviewValueFromRecord = ({
|
||||
record,
|
||||
fieldMetadataItem,
|
||||
selectOptions,
|
||||
}: {
|
||||
record: ObjectRecord;
|
||||
fieldMetadataItem: Pick<FieldMetadataItem, 'name' | 'type'>;
|
||||
selectOptions?: SettingsObjectFieldSelectFormValues;
|
||||
}) => {
|
||||
const recordFieldValue = record[fieldMetadataItem.name];
|
||||
|
||||
// Select field
|
||||
if (fieldMetadataItem.type === FieldMetadataType.Select) {
|
||||
return selectOptions?.find(
|
||||
(selectOption) => selectOption.value === recordFieldValue,
|
||||
);
|
||||
}
|
||||
|
||||
// Relation fields (to many)
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
Array.isArray(recordFieldValue?.edges)
|
||||
) {
|
||||
return recordFieldValue.edges[0]?.node;
|
||||
}
|
||||
|
||||
// Other fields
|
||||
return recordFieldValue;
|
||||
};
|
||||
@ -32,10 +32,11 @@ export type SelectProps<Value extends string | number | null> = {
|
||||
withSearchInput?: boolean;
|
||||
};
|
||||
|
||||
const StyledControlContainer = styled.div<{
|
||||
disabled?: boolean;
|
||||
fullWidth?: boolean;
|
||||
}>`
|
||||
const StyledContainer = styled.div<{ fullWidth?: boolean }>`
|
||||
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
|
||||
`;
|
||||
|
||||
const StyledControlContainer = styled.div<{ disabled?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
@ -43,7 +44,7 @@ const StyledControlContainer = styled.div<{
|
||||
color: ${({ disabled, theme }) =>
|
||||
disabled ? theme.font.color.tertiary : theme.font.color.primary};
|
||||
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||
display: ${({ fullWidth }) => (fullWidth ? 'flex' : 'inline-flex')};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
justify-content: space-between;
|
||||
@ -100,7 +101,7 @@ export const Select = <Value extends string | number | null>({
|
||||
const { closeDropdown } = useDropdown(dropdownId);
|
||||
|
||||
const selectControl = (
|
||||
<StyledControlContainer disabled={disabled} fullWidth={fullWidth}>
|
||||
<StyledControlContainer disabled={disabled}>
|
||||
<StyledControlLabel>
|
||||
{!!selectedOption?.Icon && (
|
||||
<selectedOption.Icon
|
||||
@ -115,50 +116,49 @@ export const Select = <Value extends string | number | null>({
|
||||
</StyledControlContainer>
|
||||
);
|
||||
|
||||
return disabled ? (
|
||||
<div>
|
||||
return (
|
||||
<StyledContainer className={className} fullWidth={fullWidth}>
|
||||
{!!label && <StyledLabel>{label}</StyledLabel>}
|
||||
{selectControl}
|
||||
</div>
|
||||
) : (
|
||||
<div className={className}>
|
||||
{!!label && <StyledLabel>{label}</StyledLabel>}
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownMenuWidth={dropdownWidth}
|
||||
dropdownPlacement="bottom-start"
|
||||
clickableComponent={selectControl}
|
||||
dropdownComponents={
|
||||
<>
|
||||
{!!withSearchInput && (
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
value={searchInputValue}
|
||||
onChange={(event) => setSearchInputValue(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
{!!withSearchInput && !!filteredOptions.length && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
{!!filteredOptions.length && (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{filteredOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
LeftIcon={option.Icon}
|
||||
text={option.label}
|
||||
onClick={() => {
|
||||
onChange?.(option.value);
|
||||
closeDropdown();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
|
||||
/>
|
||||
</div>
|
||||
{disabled ? (
|
||||
selectControl
|
||||
) : (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownMenuWidth={dropdownWidth}
|
||||
dropdownPlacement="bottom-start"
|
||||
clickableComponent={selectControl}
|
||||
dropdownComponents={
|
||||
<>
|
||||
{!!withSearchInput && (
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
value={searchInputValue}
|
||||
onChange={(event) => setSearchInputValue(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
{!!withSearchInput && !!filteredOptions.length && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
{!!filteredOptions.length && (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{filteredOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
LeftIcon={option.Icon}
|
||||
text={option.label}
|
||||
onClick={() => {
|
||||
onChange?.(option.value);
|
||||
closeDropdown();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledCard = styled.div`
|
||||
const StyledCard = styled.div<{ fullWidth?: boolean }>`
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
overflow: hidden;
|
||||
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
|
||||
`;
|
||||
|
||||
export { StyledCard as Card };
|
||||
|
||||
@ -7,6 +7,7 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons
|
||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
|
||||
import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { IconArchive, IconSettings } from '@/ui/display/icon';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
@ -125,6 +126,15 @@ export const SettingsObjectEdit = () => {
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Settings"
|
||||
description="Choose the fields that will identify your records"
|
||||
/>
|
||||
<SettingsDataModelObjectSettingsFormCard
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
/>
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title title="Danger zone" description="Disable object" />
|
||||
<Button
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
@ -12,8 +13,9 @@ import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderCon
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsObjectFieldCurrencyFormValues } from '@/settings/data-model/components/SettingsObjectFieldCurrencyForm';
|
||||
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
|
||||
import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection';
|
||||
import { useFieldMetadataForm } from '@/settings/data-model/hooks/useFieldMetadataForm';
|
||||
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
|
||||
import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
|
||||
import { useFieldMetadataForm } from '@/settings/data-model/fields/forms/hooks/useFieldMetadataForm';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { IconArchive, IconSettings } from '@/ui/display/icon';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
@ -27,6 +29,12 @@ import {
|
||||
RelationMetadataType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledSettingsObjectFieldTypeSelect = styled(
|
||||
SettingsDataModelFieldTypeSelect,
|
||||
)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const canPersistFieldMetadataItemUpdate = (
|
||||
fieldMetadataItem: FieldMetadataItem,
|
||||
) => {
|
||||
@ -214,23 +222,35 @@ export const SettingsObjectFieldEdit = () => {
|
||||
iconKey={formValues.icon}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
<SettingsObjectFieldTypeSelectSection
|
||||
disableCurrencyForm
|
||||
fieldMetadata={{
|
||||
icon: formValues.icon,
|
||||
label: formValues.label || 'Employees',
|
||||
id: activeMetadataField.id,
|
||||
}}
|
||||
objectMetadataId={activeObjectMetadataItem.id}
|
||||
onChange={handleFormChange}
|
||||
relationFieldMetadata={relationFieldMetadataItem}
|
||||
values={{
|
||||
type: formValues.type,
|
||||
currency: formValues.currency,
|
||||
relation: formValues.relation,
|
||||
select: formValues.select,
|
||||
}}
|
||||
/>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Type and values"
|
||||
description="The field's type and values."
|
||||
/>
|
||||
<StyledSettingsObjectFieldTypeSelect
|
||||
disabled
|
||||
onChange={handleFormChange}
|
||||
value={formValues.type}
|
||||
/>
|
||||
<SettingsDataModelFieldSettingsFormCard
|
||||
disableCurrencyForm
|
||||
fieldMetadataItem={{
|
||||
icon: formValues.icon,
|
||||
id: activeMetadataField.id,
|
||||
label: formValues.label,
|
||||
name: activeMetadataField.name,
|
||||
type: formValues.type,
|
||||
}}
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
onChange={handleFormChange}
|
||||
relationFieldMetadataItem={relationFieldMetadataItem}
|
||||
values={{
|
||||
currency: formValues.currency,
|
||||
relation: formValues.relation,
|
||||
select: formValues.select,
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
{!isLabelIdentifier && (
|
||||
<Section>
|
||||
<H2Title title="Danger zone" description="Disable this field" />
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Reference } from '@apollo/client';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { CachedObjectRecordEdge } from '@/apollo/types/CachedObjectRecordEdge';
|
||||
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
|
||||
@ -14,17 +15,26 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons
|
||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
|
||||
import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection';
|
||||
import { useFieldMetadataForm } from '@/settings/data-model/hooks/useFieldMetadataForm';
|
||||
import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard';
|
||||
import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect';
|
||||
import { useFieldMetadataForm } from '@/settings/data-model/fields/forms/hooks/useFieldMetadataForm';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { IconSettings } from '@/ui/display/icon';
|
||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||
import { View } from '@/views/types/View';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledSettingsObjectFieldTypeSelect = styled(
|
||||
SettingsDataModelFieldTypeSelect,
|
||||
)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const SettingsObjectNewFieldStep2 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { objectSlug = '' } = useParams();
|
||||
@ -286,21 +296,31 @@ export const SettingsObjectNewFieldStep2 = () => {
|
||||
description={formValues.description}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
<SettingsObjectFieldTypeSelectSection
|
||||
excludedFieldTypes={excludedFieldTypes}
|
||||
fieldMetadata={{
|
||||
icon: formValues.icon,
|
||||
label: formValues.label || 'Employees',
|
||||
}}
|
||||
objectMetadataId={activeObjectMetadataItem.id}
|
||||
onChange={handleFormChange}
|
||||
values={{
|
||||
type: formValues.type,
|
||||
currency: formValues.currency,
|
||||
relation: formValues.relation,
|
||||
select: formValues.select,
|
||||
}}
|
||||
/>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Type and values"
|
||||
description="The field's type and values."
|
||||
/>
|
||||
<StyledSettingsObjectFieldTypeSelect
|
||||
excludedFieldTypes={excludedFieldTypes}
|
||||
onChange={handleFormChange}
|
||||
value={formValues.type}
|
||||
/>
|
||||
<SettingsDataModelFieldSettingsFormCard
|
||||
fieldMetadataItem={{
|
||||
icon: formValues.icon,
|
||||
label: formValues.label || 'Employees',
|
||||
type: formValues.type,
|
||||
}}
|
||||
objectMetadataItem={activeObjectMetadataItem}
|
||||
onChange={handleFormChange}
|
||||
values={{
|
||||
currency: formValues.currency,
|
||||
relation: formValues.relation,
|
||||
select: formValues.select,
|
||||
}}
|
||||
/>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
|
||||
@ -1,11 +1,22 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Decorator } from '@storybook/react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||
|
||||
export const ObjectMetadataItemsDecorator: Decorator = (Story) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => setCurrentWorkspaceMember(mockWorkspaceMembers[0]),
|
||||
[setCurrentWorkspaceMember],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -103,11 +103,15 @@ export const graphqlMocks = {
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphql.query('FindManyCompanies', () => {
|
||||
graphql.query('FindManyCompanies', ({ variables }) => {
|
||||
const mockedData = variables.limit
|
||||
? mockedCompaniesData.slice(0, variables.limit)
|
||||
: mockedCompaniesData;
|
||||
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
companies: {
|
||||
edges: mockedCompaniesData.map((company) => ({
|
||||
edges: mockedData.map((company) => ({
|
||||
node: {
|
||||
...company,
|
||||
favorites: {
|
||||
|
||||
@ -1109,7 +1109,7 @@ export const mockedCompaniesMetadata = {
|
||||
node: {
|
||||
__typename: 'field',
|
||||
id: '726f398f-7007-49f9-8b03-f71813c9fcf2',
|
||||
type: FieldMetadataType.Text,
|
||||
type: FieldMetadataType.Select,
|
||||
name: 'industry',
|
||||
label: 'Industry',
|
||||
description: 'The industry the company is involved in',
|
||||
@ -1122,8 +1122,51 @@ export const mockedCompaniesMetadata = {
|
||||
updatedAt: '2023-12-20T12:25:25.057Z',
|
||||
fromRelationMetadata: null,
|
||||
toRelationMetadata: null,
|
||||
options: [],
|
||||
defaultValue: null,
|
||||
options: [
|
||||
{
|
||||
id: '9ecaa155-cb8a-4840-8c28-a308d4fdc35d',
|
||||
color: 'pink',
|
||||
label: '💊 Health',
|
||||
position: 0,
|
||||
value: 'HEALTH',
|
||||
},
|
||||
{
|
||||
id: '75287708-8bb6-4c69-8951-a622b2935ac7',
|
||||
color: 'purple',
|
||||
label: '🏭 Industry',
|
||||
position: 1,
|
||||
value: 'INDUSTRY',
|
||||
},
|
||||
{
|
||||
id: 'f8694cf8-c673-49ab-b758-b369de8584ec',
|
||||
color: 'sky',
|
||||
label: '🤖 SaaS',
|
||||
position: 2,
|
||||
value: 'SAAS',
|
||||
},
|
||||
{
|
||||
id: '3fd92580-1549-4cc0-b0b3-3a03bcffa7d0',
|
||||
color: 'turquoise',
|
||||
label: '🌿 Green tech',
|
||||
position: 3,
|
||||
value: 'GREEN_TECH',
|
||||
},
|
||||
{
|
||||
id: '9b4a1271-c515-4440-b450-78eb7ca05975',
|
||||
color: 'yellow',
|
||||
label: '🚲 Mobility',
|
||||
position: 4,
|
||||
value: 'MOBILITY',
|
||||
},
|
||||
{
|
||||
id: 'c80f4670-e8a5-4488-88b3-97a1b46f5940',
|
||||
color: 'green',
|
||||
label: '🌏 NGO',
|
||||
position: 5,
|
||||
value: 'NGO',
|
||||
},
|
||||
],
|
||||
defaultValue: 'HEALTH',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -10,7 +10,7 @@ export const mockWorkspaceMembers = [
|
||||
createdAt: '2023-12-18T09:51:19.645Z',
|
||||
updatedAt: '2023-12-18T09:51:19.645Z',
|
||||
userId: '20202020-7169-42cf-bc47-1cfef15264b8',
|
||||
colorScheme: 'Light',
|
||||
colorScheme: 'Light' as const,
|
||||
},
|
||||
{
|
||||
id: '20202020-77d5-4cb6-b60a-f4a835a85d61',
|
||||
@ -23,6 +23,6 @@ export const mockWorkspaceMembers = [
|
||||
createdAt: '2023-12-18T09:51:19.645Z',
|
||||
updatedAt: '2023-12-18T09:51:19.645Z',
|
||||
userId: '20202020-3957-4908-9c36-2929a23f8357',
|
||||
colorScheme: 'Light',
|
||||
colorScheme: 'Dark' as const,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user