Update Seeds while pre-fi

lling a new workspace
This commit is contained in:
Charles Bochet
2023-11-17 21:54:32 +01:00
parent e90beef91f
commit aa2596c572
66 changed files with 476 additions and 668 deletions

View File

@ -67,7 +67,7 @@ export const CommentHeader = ({ comment, actionBar }: CommentHeaderProps) => {
const showDate = beautifiedCreatedAt !== '';
const author = comment.author;
const authorName = author.firstName + ' ' + author.lastName;
const authorName = author.name.firstName + ' ' + author.name.lastName;
const avatarUrl = author.avatarUrl;
const commentId = comment.id;

View File

@ -10,8 +10,10 @@ export const mockComment: Pick<
body: 'Hello, this is a comment.',
author: {
id: 'fake_comment_1_author_uuid',
firstName: 'Jony' ?? '',
lastName: 'Ive' ?? '',
name: {
firstName: 'Jony' ?? '',
lastName: 'Ive' ?? '',
},
avatarUrl: null,
},
createdAt: DateTime.fromFormat('2021-03-12', 'yyyy-MM-dd').toISO() ?? '',
@ -26,8 +28,10 @@ export const mockCommentWithLongValues: Pick<
body: 'Hello, this is a comment. Hello, this is a comment. Hello, this is a comment. Hello, this is a comment. Hello, this is a comment. Hello, this is a comment.',
author: {
id: 'fake_comment_1_author_uuid',
firstName: 'Jony' ?? '',
lastName: 'Ive' ?? '',
name: {
firstName: 'Jony' ?? '',
lastName: 'Ive' ?? '',
},
avatarUrl: null,
},
createdAt: DateTime.fromFormat('2021-03-12', 'yyyy-MM-dd').toISO() ?? '',

View File

@ -14,10 +14,7 @@ import {
export type ActivityAssigneePickerProps = {
activity: Pick<Activity, 'id'> & {
accountOwner?: Pick<
WorkspaceMember,
'id' | 'firstName' | 'lastName'
> | null;
accountOwner?: Pick<WorkspaceMember, 'id' | 'name'> | null;
};
onSubmit?: () => void;
onCancel?: () => void;

View File

@ -60,10 +60,7 @@ type ActivityEditorProps = {
> & {
comments?: Array<Comment> | null;
} & {
assignee?: Pick<
WorkspaceMember,
'id' | 'firstName' | 'lastName' | 'avatarUrl'
> | null;
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
} & {
activityTargets?: Array<
Pick<ActivityTarget, 'id' | 'companyId' | 'personId'>

View File

@ -13,10 +13,7 @@ import { Company, User } from '~/generated/graphql';
type ActivityAssigneeEditableFieldProps = {
activity: Pick<Company, 'id' | 'accountOwnerId'> & {
assignee?: Pick<
WorkspaceMember,
'id' | 'firstName' | 'lastName' | 'avatarUrl'
> | null;
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
};
};

View File

@ -7,6 +7,7 @@ import { Activity } from '@/activities/types/Activity';
import { IconNotes } from '@/ui/display/icon';
import { OverflowingTextWithTooltip } from '@/ui/display/tooltip/OverflowingTextWithTooltip';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import {
beautifyExactDateTime,
beautifyPastDateRelativeToNow,
@ -127,11 +128,8 @@ type TimelineActivityProps = {
| 'type'
| 'comments'
| 'dueAt'
> & { author: Pick<Activity['author'], 'firstName' | 'lastName'> } & {
assignee?: Pick<
Activity['author'],
'id' | 'firstName' | 'lastName' | 'avatarUrl'
> | null;
> & { author: Pick<WorkspaceMember, 'name'> } & {
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
};
};
@ -151,7 +149,9 @@ export const TimelineActivity = ({ activity }: TimelineActivityProps) => {
</StyledIconContainer>
<StyledItemTitleContainer>
<span>
{activity.author.firstName + ' ' + activity.author.lastName}
{activity.author.name.firstName +
' ' +
activity.author.name.lastName}
</span>
created a {activity.type.toLowerCase()}
</StyledItemTitleContainer>

View File

@ -9,10 +9,7 @@ import { beautifyExactDate } from '~/utils/date-utils';
type TimelineActivityCardFooterProps = {
activity: Pick<Activity, 'id' | 'dueAt' | 'comments'> & {
assignee?: Pick<
WorkspaceMember,
'id' | 'firstName' | 'lastName' | 'avatarUrl'
> | null;
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
};
};
@ -48,9 +45,9 @@ export const TimelineActivityCardFooter = ({
<UserChip
id={activity.assignee.id}
name={
activity.assignee.firstName +
activity.assignee.name.firstName +
' ' +
activity.assignee.lastName ?? ''
activity.assignee.name.lastName ?? ''
}
pictureUrl={activity.assignee.avatarUrl ?? ''}
/>

View File

@ -15,12 +15,9 @@ export type Activity = {
type: ActivityType;
title: string;
body: string;
author: Pick<WorkspaceMember, 'id' | 'firstName' | 'lastName' | 'avatarUrl'>;
author: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'>;
authorId: string;
assignee: Pick<
WorkspaceMember,
'id' | 'firstName' | 'lastName' | 'avatarUrl'
> | null;
assignee: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
assigneeId: string | null;
comments: Comment[];
};

View File

@ -6,5 +6,5 @@ export type Comment = {
body: string;
updatedAt: string;
activityId: string;
author: Pick<WorkspaceMember, 'id' | 'firstName' | 'lastName' | 'avatarUrl'>;
author: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'>;
};

View File

@ -169,8 +169,10 @@ export const useAuth = () => {
mutation: CREATE_ONE_WORKSPACE_MEMBER_V2,
variables: {
input: {
firstName: user.firstName ?? '',
lastName: user.lastName ?? '',
name: {
firstName: user.firstName ?? '',
lastName: user.lastName ?? '',
},
colorScheme: 'Light',
userId: user.id,
allowImpersonation: true,

View File

@ -25,7 +25,10 @@ export const getOnboardingStatus = (
if (!currentWorkspace?.displayName) {
return OnboardingStatus.OngoingWorkspaceCreation;
}
if (!currentWorkspaceMember.firstName || !currentWorkspaceMember.lastName) {
if (
!currentWorkspaceMember.name.firstName ||
!currentWorkspaceMember.name.lastName
) {
return OnboardingStatus.OngoingProfileCreation;
}

View File

@ -1,4 +1,7 @@
import { Company, Favorite, User } from '../../../../generated/graphql';
import { Favorite } from '@/favorites/types/Favorite';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { Company } from '../../../../generated/graphql';
type MockedCompany = Pick<
Company,
@ -15,16 +18,7 @@ type MockedCompany = Pick<
| 'idealCustomerProfile'
| '_activityCount'
> & {
accountOwner: Pick<
User,
| 'id'
| 'email'
| 'displayName'
| 'avatarUrl'
| '__typename'
| 'firstName'
| 'lastName'
> | null;
accountOwner: Pick<WorkspaceMember, 'id' | 'avatarUrl' | 'name'> | null;
} & { Favorite: Pick<Favorite, 'id'> | null };
export const mockedCompaniesData: Array<MockedCompany> = [
@ -42,13 +36,12 @@ export const mockedCompaniesData: Array<MockedCompany> = [
Favorite: null,
_activityCount: 0,
accountOwner: {
email: 'charles@test.com',
displayName: 'Charles Test',
firstName: 'Charles',
lastName: 'Test',
name: {
firstName: 'Charles',
lastName: 'Test',
},
avatarUrl: null,
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
__typename: 'User',
},
__typename: 'Company',
},

View File

@ -91,6 +91,14 @@ export const useMapFieldMetadataToGraphQLQuery = () => {
currencyCode
}
`;
} else if (fieldType === 'FULL_NAME') {
return `
${field.name}
{
firstName
lastName
}
`;
}
};

View File

@ -91,9 +91,10 @@ export const RecordShowPage = () => {
if (isFavorite) deleteFavorite(object?.id);
else {
const additionalData =
objectNameSingular === 'peopleV2'
objectNameSingular === 'personV2'
? {
labelIdentifier: object.firstName + ' ' + object.lastName,
labelIdentifier:
object.name.firstName + ' ' + object.name.lastName,
avatarUrl: object.avatarUrl,
avatarType: 'rounded',
link: `/object/personV2/${object.id}`,
@ -114,11 +115,16 @@ export const RecordShowPage = () => {
if (!object) return <></>;
const pageName =
objectNameSingular === 'personV2'
? object.name.firstName + ' ' + object.name.lastName
: object.name;
return (
<PageContainer>
<PageTitle title={object.name || 'No Name'} />
<PageTitle title={pageName} />
<PageHeader
title={object.name ?? ''}
title={pageName ?? ''}
hasBackButton
Icon={IconBuildingSkyscraper}
>

View File

@ -4,8 +4,10 @@ export const CREATE_ONE_WORKSPACE_MEMBER_V2 = gql`
mutation CreateOneWorkspaceMemberV2($input: WorkspaceMemberV2CreateInput!) {
createWorkspaceMemberV2(data: $input) {
id
firstName
lastName
name {
firstName
lastName
}
}
}
`;

View File

@ -6,8 +6,10 @@ export const FIND_ONE_WORKSPACE_MEMBER_V2 = gql`
edges {
node {
id
firstName
lastName
name {
firstName
lastName
}
colorScheme
avatarUrl
locale

View File

@ -29,7 +29,6 @@ export const useRecordTableContextMenuEntries = () => {
const { scopeId: objectNamePlural, resetTableRowSelection } =
useRecordTable();
const { data } = useGetFavoritesQuery();
const { foundObjectMetadataItem } = useFindOneObjectMetadataItem({
objectNamePlural,
});

View File

@ -31,7 +31,7 @@ export const useGenerateFindManyCustomObjectsQuery = ({
node {
id
${objectMetadataItem.fields
.map(mapFieldMetadataToGraphQLQuery)
.map((field) => mapFieldMetadataToGraphQLQuery(field))
.join('\n')}
}
cursor

View File

@ -16,7 +16,7 @@ const PeopleTableEffect = () => {
setViewObjectMetadataId,
} = useView();
const { setAvailableTableColumns, setTableColumns } = useRecordTable();
const { setAvailableTableColumns } = useRecordTable();
useEffect(() => {
setAvailableSortDefinitions?.(personTableSortDefinitions);
@ -31,7 +31,6 @@ const PeopleTableEffect = () => {
setAvailableFilterDefinitions,
setAvailableSortDefinitions,
setAvailableTableColumns,
setTableColumns,
setViewObjectMetadataId,
setViewType,
]);

View File

@ -62,13 +62,26 @@ export const useFilteredSearchEntityQueryV2 = ({
}
return {
or: fieldNames.map((fieldName) => ({
[fieldName]: {
like: `%${filter}%`,
// TODO: fix mode
// mode: QueryMode.Insensitive,
},
})),
or: fieldNames.map((fieldName) => {
const fieldNameParts = fieldName.split('.');
if (fieldNameParts.length > 1) {
// Composite field
return {
[fieldNameParts[0]]: {
[fieldNameParts[1]]: {
ilike: `%${filter}%`,
},
},
};
}
return {
[fieldName]: {
ilike: `%${filter}%`,
},
};
}),
};
})
.filter(isDefined);

View File

@ -9,6 +9,7 @@ import {
IconPhone,
IconPlug,
IconTextSize,
IconUser,
} from '@/ui/display/icon';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { CurrencyCode, FieldMetadataType } from '~/generated-metadata/graphql';
@ -64,5 +65,6 @@ export const dataTypes: Record<
Icon: IconNumbers,
defaultValue: 50,
},
[FieldMetadataType.FullName]: { label: 'Full Name', Icon: IconUser },
[FieldMetadataType.Enum]: { label: 'Enum', Icon: IconPlug },
};

View File

@ -34,10 +34,10 @@ export const NameFields = ({
);
const [firstName, setFirstName] = useState(
currentWorkspaceMember?.firstName ?? '',
currentWorkspaceMember?.name.firstName ?? '',
);
const [lastName, setLastName] = useState(
currentWorkspaceMember?.lastName ?? '',
currentWorkspaceMember?.name.lastName ?? '',
);
const { updateOneObject, objectNotFoundInMetadata } =
@ -65,15 +65,19 @@ export const NameFields = ({
await updateOneObject({
idToUpdate: currentWorkspaceMember?.id,
input: {
firstName,
lastName,
name: {
firstName: firstName,
lastName: lastName,
},
},
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
firstName,
lastName,
name: {
firstName,
lastName,
},
});
}
} catch (error) {
@ -87,8 +91,8 @@ export const NameFields = ({
}
if (
currentWorkspaceMember.firstName !== firstName ||
currentWorkspaceMember.lastName !== lastName
currentWorkspaceMember.name.firstName !== firstName ||
currentWorkspaceMember.name.lastName !== lastName
) {
debouncedUpdate();
}

View File

@ -40,7 +40,7 @@ const SupportChat = () => {
(
chatId: string,
currentUser: Pick<User, 'email' | 'supportUserHash'>,
currentWorkspaceMember: Pick<WorkspaceMember, 'firstName' | 'lastName'>,
currentWorkspaceMember: Pick<WorkspaceMember, 'name'>,
) => {
const url = 'https://chat-assets.frontapp.com/v1/chat.bundle.js';
const script = document.querySelector(`script[src="${url}"]`);
@ -54,9 +54,9 @@ const SupportChat = () => {
useDefaultLauncher: false,
email: currentUser.email,
name:
currentWorkspaceMember.firstName +
currentWorkspaceMember.name.firstName +
' ' +
currentWorkspaceMember.lastName,
currentWorkspaceMember.name.lastName,
userHash: currentUser?.supportUserHash,
});
setIsFrontChatLoaded(true);

View File

@ -1,7 +1,9 @@
import { useContext } from 'react';
import { FullNameFieldDisplay } from '@/ui/object/field/meta-types/display/components/FullNameFieldDisplay';
import { RelationFieldDisplay } from '@/ui/object/field/meta-types/display/components/RelationFieldDisplay';
import { UuidFieldDisplay } from '@/ui/object/field/meta-types/display/components/UuidFieldDisplay';
import { isFieldFullName } from '@/ui/object/field/types/guards/isFieldFullName';
import { isFieldUuid } from '@/ui/object/field/types/guards/isFieldUuid';
import { FieldContext } from '../contexts/FieldContext';
@ -9,7 +11,6 @@ import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisp
import { CurrencyFieldDisplay } from '../meta-types/display/components/CurrencyFieldDisplay';
import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay';
import { DoubleTextChipFieldDisplay } from '../meta-types/display/components/DoubleTextChipFieldDisplay';
import { DoubleTextFieldDisplay } from '../meta-types/display/components/DoubleTextFieldDisplay';
import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay';
import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisplay';
import { MoneyFieldDisplay } from '../meta-types/display/components/MoneyFieldDisplay';
@ -20,7 +21,6 @@ import { URLFieldDisplay } from '../meta-types/display/components/URLFieldDispla
import { isFieldChip } from '../types/guards/isFieldChip';
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDoubleText } from '../types/guards/isFieldDoubleText';
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldLink } from '../types/guards/isFieldLink';
@ -56,14 +56,14 @@ export const FieldDisplay = () => {
<LinkFieldDisplay />
) : isFieldCurrency(fieldDefinition) ? (
<CurrencyFieldDisplay />
) : isFieldFullName(fieldDefinition) ? (
<FullNameFieldDisplay />
) : isFieldPhone(fieldDefinition) ? (
<PhoneFieldDisplay />
) : isFieldChip(fieldDefinition) ? (
<ChipFieldDisplay />
) : isFieldDoubleTextChip(fieldDefinition) ? (
<DoubleTextChipFieldDisplay />
) : isFieldDoubleText(fieldDefinition) ? (
<DoubleTextFieldDisplay />
) : (
<></>
)}

View File

@ -8,7 +8,6 @@ import { ChipFieldInput } from '../meta-types/input/components/ChipFieldInput';
import { CurrencyFieldInput } from '../meta-types/input/components/CurrencyFieldInput';
import { DateFieldInput } from '../meta-types/input/components/DateFieldInput';
import { DoubleTextChipFieldInput } from '../meta-types/input/components/DoubleTextChipFieldInput';
import { DoubleTextFieldInput } from '../meta-types/input/components/DoubleTextFieldInput';
import { EmailFieldInput } from '../meta-types/input/components/EmailFieldInput';
import { LinkFieldInput } from '../meta-types/input/components/LinkFieldInput';
import { MoneyFieldInput } from '../meta-types/input/components/MoneyFieldInput';
@ -23,7 +22,6 @@ import { isFieldBoolean } from '../types/guards/isFieldBoolean';
import { isFieldChip } from '../types/guards/isFieldChip';
import { isFieldCurrency } from '../types/guards/isFieldCurrency';
import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDoubleText } from '../types/guards/isFieldDoubleText';
import { isFieldDoubleTextChip } from '../types/guards/isFieldDoubleTextChip';
import { isFieldEmail } from '../types/guards/isFieldEmail';
import { isFieldLink } from '../types/guards/isFieldLink';
@ -146,14 +144,6 @@ export const FieldInput = ({
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldDoubleText(fieldDefinition) ? (
<DoubleTextFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
) : isFieldMoney(fieldDefinition) ? (
<MoneyFieldInput
onEnter={onEnter}

View File

@ -1,10 +0,0 @@
import { useDoubleTextField } from '../../hooks/useDoubleTextField';
import { TextDisplay } from '../content-display/components/TextDisplay';
export const DoubleTextFieldDisplay = () => {
const { firstValue, secondValue } = useDoubleTextField();
const content = [firstValue, secondValue].filter(Boolean).join(' ');
return <TextDisplay text={content} />;
};

View File

@ -0,0 +1,13 @@
import { useFullNameField } from '@/ui/object/field/meta-types/hooks/useFullNameField';
import { TextDisplay } from '../content-display/components/TextDisplay';
export const FullNameFieldDisplay = () => {
const { fieldValue } = useFullNameField();
const content = [fieldValue.firstName, fieldValue.lastName]
.filter(Boolean)
.join(' ');
return <TextDisplay text={content} />;
};

View File

@ -1,79 +0,0 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { FieldContext } from '../../../../contexts/FieldContext';
import { useDoubleTextField } from '../../../hooks/useDoubleTextField';
import { DoubleTextFieldDisplay } from '../DoubleTextFieldDisplay'; // Import your component
const DoubleTextFieldDisplayValueSetterEffect = ({
firstValue,
secondValue,
}: {
firstValue: string;
secondValue: string;
}) => {
const { setFirstValue, setSecondValue } = useDoubleTextField();
useEffect(() => {
setFirstValue(firstValue);
setSecondValue(secondValue);
}, [setFirstValue, setSecondValue, firstValue, secondValue]);
return null;
};
const meta: Meta = {
title: 'UI/Data/Field/Display/DoubleTextFieldDisplay',
decorators: [
(Story, { args }) => (
<FieldContext.Provider
value={{
entityId: '',
fieldDefinition: {
fieldMetadataId: 'double-text',
label: 'Double-Text',
type: 'DOUBLE_TEXT',
metadata: {
firstValueFieldName: 'First-text',
firstValuePlaceholder: 'First-text',
secondValueFieldName: 'Second-text',
secondValuePlaceholder: 'Second-text',
},
},
hotkeyScope: 'hotkey-scope',
}}
>
<DoubleTextFieldDisplayValueSetterEffect
firstValue={args.firstValue}
secondValue={args.secondValue}
/>
<Story />
</FieldContext.Provider>
),
ComponentDecorator,
],
component: DoubleTextFieldDisplay,
args: {
firstValue: 'Lorem',
secondValue: 'ipsum',
},
};
export default meta;
type Story = StoryObj<typeof DoubleTextFieldDisplay>;
export const Default: Story = {};
export const Elipsis: Story = {
args: {
firstValue:
'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
secondValue: 'ipsum dolor sit amet, consectetur adipiscing elit.',
},
parameters: {
container: { width: 100 },
},
};

View File

@ -21,12 +21,12 @@ export const getEntityChipFromFieldMetadata = (
// TODO: use every
if (fieldName === 'accountOwner' && fieldValue) {
chipValue.name = fieldValue.firstName + ' ' + fieldValue.lastName;
chipValue.name = fieldValue.name.firstName + ' ' + fieldValue.name.lastName;
} else if (fieldName === 'company' && fieldValue) {
chipValue.name = fieldValue.name;
chipValue.pictureUrl = getLogoUrlFromDomainName(fieldValue.domainName);
} else if (fieldName === 'person' && fieldValue) {
chipValue.name = fieldValue.firstName + ' ' + fieldValue.lastName;
chipValue.name = fieldValue.name.firstName + ' ' + fieldValue.name.lastName;
}
return chipValue;

View File

@ -1,54 +0,0 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldDoubleText } from '../../types/guards/isFieldDoubleText';
export const useDoubleTextField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('DOUBLE_TEXT', isFieldDoubleText, fieldDefinition);
const [firstValue, setFirstValue] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldDefinition.metadata.firstValueFieldName,
}),
);
const [secondValue, setSecondValue] = useRecoilState<string>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldDefinition.metadata.secondValueFieldName,
}),
);
const fieldInitialValue = useFieldInitialValue();
const initialFirstValue = fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value ?? firstValue;
const initialSecondValue = fieldInitialValue?.isEmpty
? ''
: fieldInitialValue?.value
? ''
: secondValue;
const fullValue = [firstValue, secondValue].filter(Boolean).join(' ');
return {
fieldDefinition,
secondValue,
setSecondValue,
firstValue,
initialFirstValue,
initialSecondValue,
setFirstValue,
fullValue,
hotkeyScope,
};
};

View File

@ -0,0 +1,51 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
import { usePersistField } from '../../hooks/usePersistField';
import { entityFieldsFamilySelector } from '../../states/selectors/entityFieldsFamilySelector';
import { FieldFullNameValue } from '../../types/FieldMetadata';
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
import { isFieldFullName } from '../../types/guards/isFieldFullName';
import { isFieldFullNameValue } from '../../types/guards/isFieldFullNameValue';
export const useFullNameField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('FULL_NAME', isFieldFullName, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;
const [fieldValue, setFieldValue] = useRecoilState<FieldFullNameValue>(
entityFieldsFamilySelector({
entityId: entityId,
fieldName: fieldName,
}),
);
const persistField = usePersistField();
const persistFullNameField = (newValue: FieldFullNameValue) => {
if (!isFieldFullNameValue(newValue)) {
return;
}
persistField(newValue);
};
const fieldInitialValue = useFieldInitialValue();
const initialValue: FieldFullNameValue = fieldInitialValue?.isEmpty
? { firstName: '', lastName: '' }
: fieldValue;
return {
fieldDefinition,
fieldValue,
initialValue,
setFieldValue,
hotkeyScope,
persistFullNameField,
};
};

View File

@ -1,73 +0,0 @@
import { DoubleTextInput } from '@/ui/object/field/meta-types/input/components/internal/DoubleTextInput';
import { FieldDoubleText } from '@/ui/object/field/types/FieldDoubleText';
import { usePersistField } from '../../../hooks/usePersistField';
import { useDoubleTextField } from '../../hooks/useDoubleTextField';
import { FieldInputOverlay } from './internal/FieldInputOverlay';
import { FieldInputEvent } from './DateFieldInput';
export type DoubleTextFieldInputProps = {
onClickOutside?: FieldInputEvent;
onEnter?: FieldInputEvent;
onEscape?: FieldInputEvent;
onTab?: FieldInputEvent;
onShiftTab?: FieldInputEvent;
};
export const DoubleTextFieldInput = ({
onEnter,
onEscape,
onClickOutside,
onTab,
onShiftTab,
}: DoubleTextFieldInputProps) => {
const {
fieldDefinition,
initialFirstValue,
initialSecondValue,
hotkeyScope,
} = useDoubleTextField();
const persistField = usePersistField();
const handleEnter = (newDoubleText: FieldDoubleText) => {
onEnter?.(() => persistField(newDoubleText));
};
const handleEscape = (newDoubleText: FieldDoubleText) => {
onEscape?.(() => persistField(newDoubleText));
};
const handleClickOutside = (
event: MouseEvent | TouchEvent,
newDoubleText: FieldDoubleText,
) => {
onClickOutside?.(() => persistField(newDoubleText));
};
const handleTab = (newDoubleText: FieldDoubleText) => {
onTab?.(() => persistField(newDoubleText));
};
const handleShiftTab = (newDoubleText: FieldDoubleText) => {
onShiftTab?.(() => persistField(newDoubleText));
};
return (
<FieldInputOverlay>
<DoubleTextInput
firstValue={initialFirstValue}
secondValue={initialSecondValue}
firstValuePlaceholder={fieldDefinition.metadata.firstValuePlaceholder}
secondValuePlaceholder={fieldDefinition.metadata.secondValuePlaceholder}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
onShiftTab={handleShiftTab}
onTab={handleTab}
hotkeyScope={hotkeyScope}
/>
</FieldInputOverlay>
);
};

View File

@ -1,191 +0,0 @@
import { useEffect } from 'react';
import { expect, jest } from '@storybook/jest';
import { Decorator, Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
import { useDoubleTextField } from '../../../hooks/useDoubleTextField';
import {
DoubleTextFieldInput,
DoubleTextFieldInputProps,
} from '../DoubleTextFieldInput';
const DoubleTextFieldValueSetterEffect = ({
firstValue,
secondValue,
}: {
firstValue: string;
secondValue: string;
}) => {
const { setFirstValue, setSecondValue } = useDoubleTextField();
useEffect(() => {
setFirstValue(firstValue);
setSecondValue(secondValue);
}, [firstValue, secondValue, setFirstValue, setSecondValue]);
return <></>;
};
type DoubleTextFieldInputWithContextProps = DoubleTextFieldInputProps & {
firstValue: string;
secondValue: string;
entityId?: string;
};
const DoubleTextFieldInputWithContext = ({
entityId,
firstValue,
secondValue,
onClickOutside,
onEnter,
onEscape,
onTab,
onShiftTab,
}: DoubleTextFieldInputWithContextProps) => {
const setHotKeyScope = useSetHotkeyScope();
useEffect(() => {
setHotKeyScope('hotkey-scope');
}, [setHotKeyScope]);
return (
<div>
<FieldContextProvider
fieldDefinition={{
fieldMetadataId: 'double-text',
label: 'Double-Text',
type: 'DOUBLE_TEXT',
metadata: {
firstValueFieldName: 'First-text',
firstValuePlaceholder: 'First-text',
secondValueFieldName: 'Second-text',
secondValuePlaceholder: 'Second-text',
},
}}
entityId={entityId}
>
<DoubleTextFieldValueSetterEffect {...{ firstValue, secondValue }} />
<DoubleTextFieldInput
onEnter={onEnter}
onEscape={onEscape}
onClickOutside={onClickOutside}
onTab={onTab}
onShiftTab={onShiftTab}
/>
</FieldContextProvider>
<div data-testid="data-field-input-click-outside-div" />
</div>
);
};
const enterJestFn = jest.fn();
const escapeJestfn = jest.fn();
const clickOutsideJestFn = jest.fn();
const tabJestFn = jest.fn();
const shiftTabJestFn = jest.fn();
const clearMocksDecorator: Decorator = (Story, context) => {
if (context.parameters.clearMocks) {
enterJestFn.mockClear();
escapeJestfn.mockClear();
clickOutsideJestFn.mockClear();
tabJestFn.mockClear();
shiftTabJestFn.mockClear();
}
return <Story />;
};
const meta: Meta = {
title: 'UI/Data/Field/Input/DoubleTextFieldInput',
component: DoubleTextFieldInputWithContext,
args: {
firstValue: 'first value',
secondValue: 'second value',
onEnter: enterJestFn,
onEscape: escapeJestfn,
onClickOutside: clickOutsideJestFn,
onTab: tabJestFn,
onShiftTab: shiftTabJestFn,
},
argTypes: {
onEnter: { control: false },
onEscape: { control: false },
onClickOutside: { control: false },
onTab: { control: false },
onShiftTab: { control: false },
},
decorators: [clearMocksDecorator],
parameters: {
clearMocks: true,
},
};
export default meta;
type Story = StoryObj<typeof DoubleTextFieldInputWithContext>;
export const Default: Story = {};
export const Enter: Story = {
play: async () => {
expect(enterJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{enter}');
expect(enterJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Escape: Story = {
play: async () => {
expect(escapeJestfn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{esc}');
expect(escapeJestfn).toHaveBeenCalledTimes(1);
});
},
};
export const ClickOutside: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
const emptyDiv = await canvas.findByTestId(
'data-field-input-click-outside-div',
);
await waitFor(() => {
userEvent.click(emptyDiv);
expect(clickOutsideJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const Tab: Story = {
play: async () => {
expect(tabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{tab}');
expect(tabJestFn).toHaveBeenCalledTimes(1);
});
},
};
export const ShiftTab: Story = {
play: async () => {
expect(shiftTabJestFn).toHaveBeenCalledTimes(0);
await waitFor(() => {
userEvent.keyboard('{shift>}{tab}');
expect(shiftTabJestFn).toHaveBeenCalledTimes(1);
});
},
};

View File

@ -1,5 +1,7 @@
import { selectorFamily } from 'recoil';
import { isFieldFullName } from '@/ui/object/field/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/ui/object/field/types/guards/isFieldFullNameValue';
import { isFieldUuid } from '@/ui/object/field/types/guards/isFieldUuid';
import { assertNotNull } from '~/utils/assert';
@ -108,6 +110,16 @@ export const isEntityFieldEmptyFamilySelector = selectorFamily({
);
}
if (isFieldFullName(fieldDefinition)) {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName];
return (
!isFieldFullNameValue(fieldValue) ||
isValueEmpty(fieldValue?.firstName + fieldValue?.lastName)
);
}
if (isFieldLink(fieldDefinition)) {
const fieldName = fieldDefinition.metadata.fieldName;
const fieldValue = get(entityFieldsFamilyState(entityId))?.[fieldName];

View File

@ -48,6 +48,10 @@ export type FieldCurrencyMetadata = {
isPositive?: boolean;
};
export type FieldFullnameMetadata = {
fieldName: string;
};
export type FieldEmailMetadata = {
fieldName: string;
placeHolder: string;
@ -119,6 +123,7 @@ export type FieldLinkValue = { url: string; label: string };
export type FieldNumberValue = number | null;
export type FieldMoneyValue = number | null;
export type FieldCurrencyValue = { currencyCode: string; amountMicros: number };
export type FieldFullNameValue = { firstName: string; lastName: string };
export type FieldEmailValue = string;
export type FieldProbabilityValue = number;

View File

@ -15,4 +15,5 @@ export type FieldType =
| 'PROBABILITY'
| 'CURRENCY'
| 'MONEY_AMOUNT'
| 'MONEY';
| 'MONEY'
| 'FULL_NAME';

View File

@ -7,6 +7,7 @@ import {
FieldDoubleTextChipMetadata,
FieldDoubleTextMetadata,
FieldEmailMetadata,
FieldFullnameMetadata,
FieldLinkMetadata,
FieldMetadata,
FieldMoneyMetadata,
@ -28,6 +29,8 @@ type AssertFieldMetadataFunction = <
? FieldChipMetadata
: E extends 'CURRENCY'
? FieldCurrencyMetadata
: E extends 'FULL_NAME'
? FieldFullnameMetadata
: E extends 'DATE'
? FieldDateMetadata
: E extends 'DOUBLE_TEXT'

View File

@ -0,0 +1,7 @@
import { FieldDefinition } from '../FieldDefinition';
import { FieldCurrencyMetadata, FieldMetadata } from '../FieldMetadata';
export const isFieldFullName = (
field: FieldDefinition<FieldMetadata>,
): field is FieldDefinition<FieldCurrencyMetadata> =>
field.type === 'FULL_NAME';

View File

@ -0,0 +1,13 @@
import { z } from 'zod';
import { FieldFullNameValue } from '../FieldMetadata';
const currencySchema = z.object({
firstName: z.string(),
lastName: z.string(),
});
export const isFieldFullNameValue = (
fieldValue: unknown,
): fieldValue is FieldFullNameValue =>
currencySchema.safeParse(fieldValue).success;

View File

@ -6,7 +6,9 @@ import { useUpdateOneObjectRecord } from '@/object-record/hooks/useUpdateOneObje
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
export const useColorScheme = () => {
const [currentWorkspaceMember] = useRecoilState(currentWorkspaceMemberState);
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
currentWorkspaceMemberState,
);
const { updateOneObject: updateOneWorkspaceMember } =
useUpdateOneObjectRecord({
@ -19,6 +21,15 @@ export const useColorScheme = () => {
if (!currentWorkspaceMember) {
return;
}
setCurrentWorkspaceMember((current) => {
if (!current) {
return current;
}
return {
...current,
colorScheme: value,
};
});
await updateOneWorkspaceMember?.({
idToUpdate: currentWorkspaceMember?.id,
input: {
@ -26,7 +37,11 @@ export const useColorScheme = () => {
},
});
},
[currentWorkspaceMember, updateOneWorkspaceMember],
[
currentWorkspaceMember,
setCurrentWorkspaceMember,
updateOneWorkspaceMember,
],
);
return {

View File

@ -33,25 +33,26 @@ export const UserPicker = ({
}, [initialSearchFilter, setRelationPickerSearchFilter]);
const { findManyQuery } = useFindOneObjectMetadataItem({
objectNamePlural: 'workspaceMembersV2',
objectNameSingular: 'workspaceMemberV2',
});
const useFindManyWorkspaceMembers = (options: any) =>
useQuery(findManyQuery, options);
const users = useFilteredSearchEntityQueryV2({
const workspaceMembers = useFilteredSearchEntityQueryV2({
queryHook: useFindManyWorkspaceMembers,
filters: [
{
fieldNames: ['firstName', 'lastName'],
fieldNames: ['name.firstName', 'name.lastName'],
filter: relationPickerSearchFilter,
},
],
orderByField: 'firstName',
orderByField: 'createdAt',
mappingFunction: (workspaceMember) => ({
entityType: Entity.WorkspaceMember,
id: workspaceMember.id,
name: workspaceMember.firstName,
name:
workspaceMember.name.firstName + ' ' + workspaceMember.name.lastName,
avatarType: 'rounded',
avatarUrl: '',
originalEntity: workspaceMember,
@ -68,11 +69,11 @@ export const UserPicker = ({
<SingleEntitySelect
EmptyIcon={IconUserCircle}
emptyLabel="No Owner"
entitiesToSelect={users.entitiesToSelect}
loading={users.loading}
entitiesToSelect={workspaceMembers.entitiesToSelect}
loading={workspaceMembers.loading}
onCancel={onCancel}
onEntitySelected={handleEntitySelected}
selectedEntity={users.selectedEntities[0]}
selectedEntity={workspaceMembers.selectedEntities[0]}
width={width}
/>
);

View File

@ -67,7 +67,7 @@ export const ViewBarEffect = () => {
useFindManyObjectRecords({
skip: !currentViewId,
objectNamePlural: 'viewFieldsV2',
filter: { view: { eq: currentViewId } },
filter: { viewId: { eq: currentViewId } },
onCompleted: useRecoilCallback(
({ snapshot, set }) =>
async (data: PaginatedObjectTypeResults<ViewField>) => {

View File

@ -41,7 +41,7 @@ export const useViewFields = (viewScopeId: string) => {
variables: {
input: {
fieldMetadataId: viewField.fieldMetadataId,
view: viewIdToPersist,
viewId: viewIdToPersist,
isVisible: viewField.isVisible,
size: viewField.size,
position: viewField.position,

View File

@ -2,8 +2,10 @@ export type ColorScheme = 'Dark' | 'Light' | 'System';
export type WorkspaceMember = {
id: string;
firstName: string;
lastName: string;
name: {
firstName: string;
lastName: string;
};
avatarUrl: string | null;
locale: string;
colorScheme: ColorScheme;

View File

@ -41,16 +41,18 @@ export const WorkspaceMemberCard = ({
<Avatar
avatarUrl={workspaceMember.avatarUrl}
colorId={workspaceMember.id}
placeholder={workspaceMember.firstName || ''}
placeholder={workspaceMember.name.firstName || ''}
type="squared"
size="xl"
/>
<StyledContent>
<OverflowingTextWithTooltip
text={workspaceMember.firstName + ' ' + workspaceMember.lastName}
text={
workspaceMember.name.firstName + ' ' + workspaceMember.name.lastName
}
/>
<StyledEmailText>
{workspaceMember.firstName + ' ' + workspaceMember.lastName}
{workspaceMember.name.firstName + ' ' + workspaceMember.name.lastName}
</StyledEmailText>
</StyledContent>