Use FieldDefinition for company show page (#1171)

* Use FieldDefinition for company show page

* removing console.log

* fix conflicts

* fix address placeholder + company show page field definition ordering

* fix story

* add replacePlaceholder

* use AppPath enum in stories

* add routeParams

* fix people input story
This commit is contained in:
Weiko
2023-08-11 14:31:52 -07:00
committed by GitHub
parent 3978ef4edb
commit 4eb4d1488c
43 changed files with 463 additions and 478 deletions

View File

@ -1,46 +0,0 @@
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { IconUserCircle } from '@/ui/icon';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { UserChip } from '@/users/components/UserChip';
import { Company, User } from '~/generated/graphql';
import { CompanyAccountOwnerPickerFieldEditMode } from './CompanyAccountOwnerPickerFieldEditMode';
type OwnProps = {
company: Pick<Company, 'id' | 'accountOwnerId'> & {
accountOwner?: Pick<User, 'id' | 'displayName' | 'avatarUrl'> | null;
};
};
export function CompanyAccountOwnerEditableField({ company }: OwnProps) {
return (
<RecoilScope SpecificContext={FieldContext}>
<RecoilScope>
<EditableField
customEditHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,
}}
iconLabel={<IconUserCircle />}
editModeContent={
<CompanyAccountOwnerPickerFieldEditMode company={company} />
}
displayModeContent={
company.accountOwner?.displayName ? (
<UserChip
id={company.accountOwner.id}
name={company.accountOwner?.displayName ?? ''}
pictureUrl={company.accountOwner?.avatarUrl ?? ''}
/>
) : (
<></>
)
}
isDisplayModeContentEmpty={!company.accountOwner}
isDisplayModeFixHeight={true}
/>
</RecoilScope>
</RecoilScope>
);
}

View File

@ -1,47 +0,0 @@
import styled from '@emotion/styled';
import { CompanyAccountOwnerPicker } from '@/companies/components/CompanyAccountOwnerPicker';
import { useEditableField } from '@/ui/editable-field/hooks/useEditableField';
import { Company, User } from '~/generated/graphql';
const StyledContainer = styled.div`
left: 0px;
position: absolute;
top: -8px;
`;
export type OwnProps = {
company: Pick<Company, 'id'> & {
accountOwner?: Pick<User, 'id' | 'displayName'> | null;
};
onSubmit?: () => void;
onCancel?: () => void;
};
export function CompanyAccountOwnerPickerFieldEditMode({
company,
onSubmit,
onCancel,
}: OwnProps) {
const { closeEditableField } = useEditableField();
function handleSubmit() {
closeEditableField();
onSubmit?.();
}
function handleCancel() {
closeEditableField();
onCancel?.();
}
return (
<StyledContainer>
<CompanyAccountOwnerPicker
company={company}
onCancel={handleCancel}
onSubmit={handleSubmit}
/>
</StyledContainer>
);
}

View File

@ -1,65 +0,0 @@
import { useEffect, useState } from 'react';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { IconMap } from '@/ui/icon';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql';
type OwnProps = {
company: Pick<Company, 'id' | 'address'>;
};
export function CompanyAddressEditableField({ company }: OwnProps) {
const [internalValue, setInternalValue] = useState(company.address);
const [updateCompany] = useUpdateOneCompanyMutation();
useEffect(() => {
setInternalValue(company.address);
}, [company.address]);
async function handleChange(newValue: string) {
setInternalValue(newValue);
}
async function handleSubmit() {
await updateCompany({
variables: {
where: {
id: company.id,
},
data: {
address: internalValue ?? '',
},
},
});
}
async function handleCancel() {
setInternalValue(company.address);
}
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
iconLabel={<IconMap />}
editModeContent={
<TextInputEdit
placeholder={'Address'}
autoFocus
value={internalValue}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
}
displayModeContent={internalValue ?? ''}
isDisplayModeContentEmpty={!(internalValue !== '')}
/>
</RecoilScope>
);
}

View File

@ -1,79 +0,0 @@
import { useEffect, useState } from 'react';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { EditableFieldEditModeDate } from '@/ui/editable-field/variants/components/EditableFieldEditModeDate';
import { IconCalendar } from '@/ui/icon';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql';
import { formatToHumanReadableDate } from '~/utils';
import { parseDate } from '~/utils/date-utils';
type OwnProps = {
company: Pick<Company, 'id' | 'createdAt'>;
};
export function CompanyCreatedAtEditableField({ company }: OwnProps) {
const [internalValue, setInternalValue] = useState(company.createdAt);
const [updateCompany] = useUpdateOneCompanyMutation();
useEffect(() => {
setInternalValue(company.createdAt);
}, [company.createdAt]);
// TODO: refactor change and submit
async function handleChange(newValue: string) {
setInternalValue(newValue);
await updateCompany({
variables: {
where: {
id: company.id,
},
data: {
createdAt: newValue ?? '',
},
},
});
}
async function handleSubmit() {
await updateCompany({
variables: {
where: {
id: company.id,
},
data: {
createdAt: internalValue ?? '',
},
},
});
}
async function handleCancel() {
setInternalValue(company.createdAt);
}
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
iconLabel={<IconCalendar />}
editModeContent={
<EditableFieldEditModeDate
value={internalValue}
onChange={handleChange}
/>
}
displayModeContent={
internalValue !== ''
? formatToHumanReadableDate(parseDate(internalValue).toJSDate())
: 'No date'
}
isDisplayModeContentEmpty={!(internalValue !== '')}
/>
</RecoilScope>
);
}

View File

@ -1,67 +0,0 @@
import { useEffect, useState } from 'react';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldDisplayURL } from '@/ui/editable-field/components/FieldDisplayURL';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { IconLink } from '@/ui/icon';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql';
type OwnProps = {
company: Pick<Company, 'id' | 'domainName'>;
};
export function CompanyDomainNameEditableField({ company }: OwnProps) {
const [internalValue, setInternalValue] = useState(company.domainName);
const [updateCompany] = useUpdateOneCompanyMutation();
useEffect(() => {
setInternalValue(company.domainName);
}, [company.domainName]);
async function handleChange(newValue: string) {
setInternalValue(newValue);
}
async function handleSubmit() {
await updateCompany({
variables: {
where: {
id: company.id,
},
data: {
domainName: internalValue ?? '',
},
},
});
}
async function handleCancel() {
setInternalValue(company.domainName);
}
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
iconLabel={<IconLink />}
onCancel={handleCancel}
onSubmit={handleSubmit}
editModeContent={
<TextInputEdit
placeholder={'URL'}
autoFocus
value={internalValue}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
}
displayModeContent={<FieldDisplayURL URL={internalValue} />}
useEditButton
isDisplayModeContentEmpty={!(internalValue !== '')}
/>
</RecoilScope>
);
}

View File

@ -1,80 +0,0 @@
import { useEffect, useState } from 'react';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { IconUsers } from '@/ui/icon';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql';
import {
canBeCastAsIntegerOrNull,
castAsIntegerOrNull,
} from '~/utils/cast-as-integer-or-null';
type OwnProps = {
company: Pick<Company, 'id' | 'employees'>;
};
export function CompanyEmployeesEditableField({ company }: OwnProps) {
const [internalValue, setInternalValue] = useState(
company.employees?.toString(),
);
const [updateCompany] = useUpdateOneCompanyMutation();
useEffect(() => {
setInternalValue(company.employees?.toString());
}, [company.employees]);
async function handleChange(newValue: string) {
setInternalValue(newValue);
}
async function handleSubmit() {
if (!canBeCastAsIntegerOrNull(internalValue)) {
handleCancel();
return;
}
const valueCastedAsNumberOrNull = castAsIntegerOrNull(internalValue);
await updateCompany({
variables: {
where: {
id: company.id,
},
data: {
employees: valueCastedAsNumberOrNull,
},
},
});
setInternalValue(valueCastedAsNumberOrNull?.toString());
}
async function handleCancel() {
setInternalValue(company.employees?.toString());
}
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
iconLabel={<IconUsers />}
editModeContent={
<TextInputEdit
placeholder={'Employees'}
autoFocus
value={internalValue ?? ''}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
}
displayModeContent={internalValue}
isDisplayModeContentEmpty={!(internalValue && internalValue !== '0')}
/>
</RecoilScope>
);
}

View File

@ -1,5 +1,7 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import { useSetRecoilState } from 'recoil';
import { genericEntitiesFamilyState } from '@/ui/editable-field/states/genericEntitiesFamilyState';
import { useGetCompanyQuery } from '~/generated/graphql'; import { useGetCompanyQuery } from '~/generated/graphql';
export const GET_COMPANY = gql` export const GET_COMPANY = gql`
@ -33,5 +35,13 @@ export const GET_COMPANY = gql`
`; `;
export function useCompanyQuery(id: string) { export function useCompanyQuery(id: string) {
return useGetCompanyQuery({ variables: { where: { id } } }); const updateCompanyShowPage = useSetRecoilState(
genericEntitiesFamilyState(id),
);
return useGetCompanyQuery({
variables: { where: { id } },
onCompleted: (data) => {
updateCompanyShowPage(data?.findUniqueCompany);
},
});
} }

View File

@ -61,6 +61,7 @@ export const pipelineViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
type: 'relation', type: 'relation',
fieldName: 'pointOfContact', fieldName: 'pointOfContact',
relationType: Entity.Person, relationType: Entity.Person,
useEditButton: true,
}, },
isVisible: true, isVisible: true,
} satisfies ViewFieldDefinition<ViewFieldRelationMetadata>, } satisfies ViewFieldDefinition<ViewFieldRelationMetadata>,

View File

@ -1,7 +1,10 @@
import { createContext } from 'react'; import { createContext } from 'react';
import { FieldDefinition } from '@/ui/editable-field/types/FieldDefinition'; import { FieldDefinition } from '@/ui/editable-field/types/FieldDefinition';
import { FieldMetadata } from '@/ui/editable-field/types/FieldMetadata'; import {
FieldMetadata,
FieldType,
} from '@/ui/editable-field/types/FieldMetadata';
export const FieldDefinitionContext = createContext< export const FieldDefinitionContext = createContext<
FieldDefinition<FieldMetadata> FieldDefinition<FieldMetadata>
@ -9,6 +12,6 @@ export const FieldDefinitionContext = createContext<
id: '', id: '',
label: '', label: '',
icon: undefined, icon: undefined,
type: '', type: 'unknown' satisfies FieldType,
metadata: {} as FieldMetadata, metadata: {} as FieldMetadata,
}); });

View File

@ -5,10 +5,14 @@ import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldNumber } from '../types/guards/isFieldNumber'; import { isFieldNumber } from '../types/guards/isFieldNumber';
import { isFieldProbability } from '../types/guards/isFieldProbability'; import { isFieldProbability } from '../types/guards/isFieldProbability';
import { isFieldRelation } from '../types/guards/isFieldRelation'; import { isFieldRelation } from '../types/guards/isFieldRelation';
import { isFieldText } from '../types/guards/isFieldText';
import { isFieldURL } from '../types/guards/isFieldURL';
import { GenericEditableDateField } from './GenericEditableDateField'; import { GenericEditableDateField } from './GenericEditableDateField';
import { GenericEditableNumberField } from './GenericEditableNumberField'; import { GenericEditableNumberField } from './GenericEditableNumberField';
import { GenericEditableRelationField } from './GenericEditableRelationField'; import { GenericEditableRelationField } from './GenericEditableRelationField';
import { GenericEditableTextField } from './GenericEditableTextField';
import { GenericEditableURLField } from './GenericEditableURLField';
import { ProbabilityEditableField } from './ProbabilityEditableField'; import { ProbabilityEditableField } from './ProbabilityEditableField';
export function GenericEditableField() { export function GenericEditableField() {
@ -22,9 +26,13 @@ export function GenericEditableField() {
return <GenericEditableNumberField />; return <GenericEditableNumberField />;
} else if (isFieldProbability(fieldDefinition)) { } else if (isFieldProbability(fieldDefinition)) {
return <ProbabilityEditableField />; return <ProbabilityEditableField />;
} else if (isFieldURL(fieldDefinition)) {
return <GenericEditableURLField />;
} else if (isFieldText(fieldDefinition)) {
return <GenericEditableTextField />;
} else { } else {
console.warn( console.warn(
`Unknown field metadata type: ${fieldDefinition.metadata.type} in GenericEditableCell`, `Unknown field metadata type: ${fieldDefinition.type} in GenericEditableField`,
); );
return <></>; return <></>;
} }

View File

@ -69,6 +69,7 @@ export function GenericEditableNumberFieldEditMode() {
<div ref={wrapperRef}> <div ref={wrapperRef}>
<TextInputEdit <TextInputEdit
autoFocus autoFocus
placeholder={currentEditableFieldDefinition.metadata.placeHolder}
value={internalValue ? internalValue.toString() : ''} value={internalValue ? internalValue.toString() : ''}
onChange={(newValue: string) => { onChange={(newValue: string) => {
handleChange(newValue); handleChange(newValue);

View File

@ -34,7 +34,7 @@ export function GenericEditableRelationField() {
<RecoilScope SpecificContext={FieldContext}> <RecoilScope SpecificContext={FieldContext}>
<RecoilScope> <RecoilScope>
<EditableField <EditableField
useEditButton useEditButton={currentEditableFieldDefinition.metadata.useEditButton}
customEditHotkeyScope={{ customEditHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker, scope: RelationPickerHotkeyScope.RelationPicker,
}} }}

View File

@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
import { PersonChip } from '@/people/components/PersonChip'; import { PersonChip } from '@/people/components/PersonChip';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { UserChip } from '@/users/components/UserChip';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext'; import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext'; import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
@ -35,6 +36,15 @@ export function GenericEditableRelationFieldDisplayMode() {
/> />
); );
} }
case Entity.User: {
return (
<UserChip
id={fieldValue?.id ?? ''}
name={fieldValue?.displayName ?? ''}
pictureUrl={fieldValue?.avatarUrl ?? ''}
/>
);
}
default: default:
console.warn( console.warn(
`Unknown relation type: "${currentEditableFieldDefinition.metadata.relationType}" `Unknown relation type: "${currentEditableFieldDefinition.metadata.relationType}"

View File

@ -3,9 +3,9 @@ import styled from '@emotion/styled';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { PeoplePicker } from '@/people/components/PeoplePicker'; import { PeoplePicker } from '@/people/components/PeoplePicker';
import { ViewFieldRelationValue } from '@/ui/editable-field/types/ViewField';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { UserPicker } from '@/users/components/UserPicker';
import { useEditableField } from '../hooks/useEditableField'; import { useEditableField } from '../hooks/useEditableField';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField'; import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
@ -13,7 +13,10 @@ import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitio
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext'; import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector'; import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition'; import { FieldDefinition } from '../types/FieldDefinition';
import { FieldRelationMetadata } from '../types/FieldMetadata'; import {
FieldRelationMetadata,
FieldRelationValue,
} from '../types/FieldMetadata';
const RelationPickerContainer = styled.div` const RelationPickerContainer = styled.div`
left: 0px; left: 0px;
@ -28,7 +31,7 @@ function RelationPicker({
handleCancel, handleCancel,
}: { }: {
fieldDefinition: FieldDefinition<FieldRelationMetadata>; fieldDefinition: FieldDefinition<FieldRelationMetadata>;
fieldValue: ViewFieldRelationValue; fieldValue: FieldRelationValue;
handleEntitySubmit: (newRelationId: EntityForSelect | null) => void; handleEntitySubmit: (newRelationId: EntityForSelect | null) => void;
handleCancel: () => void; handleCancel: () => void;
}) { }) {
@ -36,7 +39,16 @@ function RelationPicker({
case Entity.Person: { case Entity.Person: {
return ( return (
<PeoplePicker <PeoplePicker
personId={fieldValue?.id ?? null} personId={fieldValue ? fieldValue.id : ''}
onSubmit={handleEntitySubmit}
onCancel={handleCancel}
/>
);
}
case Entity.User: {
return (
<UserPicker
userId={fieldValue ? fieldValue.id : ''}
onSubmit={handleEntitySubmit} onSubmit={handleEntitySubmit}
onCancel={handleCancel} onCancel={handleCancel}
/> />
@ -46,7 +58,7 @@ function RelationPicker({
console.warn( console.warn(
`Unknown relation type: "${fieldDefinition.metadata.relationType}" in GenericEditableRelationField`, `Unknown relation type: "${fieldDefinition.metadata.relationType}" in GenericEditableRelationField`,
); );
return <> </>; return <></>;
} }
} }

View File

@ -0,0 +1,41 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
import { FieldContext } from '../states/FieldContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldNumberMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField';
import { GenericEditableTextFieldEditMode } from './GenericEditableTextFieldEditMode';
export function GenericEditableTextField() {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldNumberMetadata>;
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
iconLabel={currentEditableFieldDefinition.icon}
editModeContent={<GenericEditableTextFieldEditMode />}
displayModeContent={fieldValue}
isDisplayModeContentEmpty={!fieldValue}
/>
</RecoilScope>
);
}

View File

@ -0,0 +1,72 @@
import { useContext, useRef, useState } from 'react';
import { useRecoilState } from 'recoil';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldTextMetadata } from '../types/FieldMetadata';
export function GenericEditableTextFieldEditMode() {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldTextMetadata>;
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const [internalValue, setInternalValue] = useState(fieldValue);
const updateField = useUpdateGenericEntityField();
const wrapperRef = useRef(null);
useRegisterCloseFieldHandlers(wrapperRef, handleSubmit, onCancel);
function handleSubmit() {
if (internalValue === fieldValue) return;
setFieldValue(internalValue);
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
internalValue,
);
}
}
function onCancel() {
setFieldValue(fieldValue);
}
function handleChange(newValue: string) {
setInternalValue(newValue);
}
return (
<div ref={wrapperRef}>
<TextInputEdit
autoFocus
placeholder={currentEditableFieldDefinition.metadata.placeHolder}
value={internalValue}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
</div>
);
}

View File

@ -0,0 +1,43 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
import { FieldContext } from '../states/FieldContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldNumberMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField';
import { FieldDisplayURL } from './FieldDisplayURL';
import { GenericEditableURLFieldEditMode } from './GenericEditableURLFieldEditMode';
export function GenericEditableURLField() {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldNumberMetadata>;
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
useEditButton
iconLabel={currentEditableFieldDefinition.icon}
editModeContent={<GenericEditableURLFieldEditMode />}
displayModeContent={<FieldDisplayURL URL={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue}
/>
</RecoilScope>
);
}

View File

@ -0,0 +1,74 @@
import { useContext, useRef, useState } from 'react';
import { useRecoilState } from 'recoil';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldURLMetadata } from '../types/FieldMetadata';
// This one is very similar to GenericEditableTextFieldEditMode
// We could probably merge them since FieldURLMetadata is basically a FieldTextMetadata
export function GenericEditableURLFieldEditMode() {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldURLMetadata>;
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const [internalValue, setInternalValue] = useState(fieldValue);
const updateField = useUpdateGenericEntityField();
const wrapperRef = useRef(null);
useRegisterCloseFieldHandlers(wrapperRef, handleSubmit, onCancel);
function handleSubmit() {
if (internalValue === fieldValue) return;
setFieldValue(internalValue);
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
internalValue,
);
}
}
function onCancel() {
setFieldValue(fieldValue);
}
function handleChange(newValue: string) {
setInternalValue(newValue);
}
return (
<div ref={wrapperRef}>
<TextInputEdit
autoFocus
placeholder={currentEditableFieldDefinition.metadata.placeHolder}
value={internalValue}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
</div>
);
}

View File

@ -1,7 +1,5 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { isFieldChip } from '@/ui/editable-field/types/guards/isFieldChip';
import { EditableFieldMutationContext } from '../states/EditableFieldMutationContext'; import { EditableFieldMutationContext } from '../states/EditableFieldMutationContext';
import { FieldDefinition } from '../types/FieldDefinition'; import { FieldDefinition } from '../types/FieldDefinition';
import { import {
@ -27,6 +25,7 @@ import {
FieldURLMetadata, FieldURLMetadata,
FieldURLValue, FieldURLValue,
} from '../types/FieldMetadata'; } from '../types/FieldMetadata';
import { isFieldChip } from '../types/guards/isFieldChip';
import { isFieldChipValue } from '../types/guards/isFieldChipValue'; import { isFieldChipValue } from '../types/guards/isFieldChipValue';
import { isFieldDate } from '../types/guards/isFieldDate'; import { isFieldDate } from '../types/guards/isFieldDate';
import { isFieldDateValue } from '../types/guards/isFieldDateValue'; import { isFieldDateValue } from '../types/guards/isFieldDateValue';
@ -53,31 +52,30 @@ export function useUpdateGenericEntityField() {
const [updateEntity] = useUpdateEntityMutation(); const [updateEntity] = useUpdateEntityMutation();
return function updateEntityField< return function updateEntityField<
MetadataType extends FieldMetadata, ValueType extends FieldMetadata extends FieldDoubleTextMetadata
ValueType extends MetadataType extends FieldDoubleTextMetadata
? FieldDoubleTextValue ? FieldDoubleTextValue
: MetadataType extends FieldTextMetadata : FieldMetadata extends FieldTextMetadata
? FieldTextValue ? FieldTextValue
: MetadataType extends FieldPhoneMetadata : FieldMetadata extends FieldPhoneMetadata
? FieldPhoneValue ? FieldPhoneValue
: MetadataType extends FieldURLMetadata : FieldMetadata extends FieldURLMetadata
? FieldURLValue ? FieldURLValue
: MetadataType extends FieldNumberMetadata : FieldMetadata extends FieldNumberMetadata
? FieldNumberValue ? FieldNumberValue
: MetadataType extends FieldDateMetadata : FieldMetadata extends FieldDateMetadata
? FieldDateValue ? FieldDateValue
: MetadataType extends FieldChipMetadata : FieldMetadata extends FieldChipMetadata
? FieldChipValue ? FieldChipValue
: MetadataType extends FieldDoubleTextChipMetadata : FieldMetadata extends FieldDoubleTextChipMetadata
? FieldDoubleTextChipValue ? FieldDoubleTextChipValue
: MetadataType extends FieldRelationMetadata : FieldMetadata extends FieldRelationMetadata
? FieldRelationValue ? FieldRelationValue
: MetadataType extends FieldProbabilityMetadata : FieldMetadata extends FieldProbabilityMetadata
? FieldProbabilityValue ? FieldProbabilityValue
: unknown, : unknown,
>( >(
currentEntityId: string, currentEntityId: string,
field: FieldDefinition<MetadataType>, field: FieldDefinition<FieldMetadata>,
newFieldValue: ValueType, newFieldValue: ValueType,
) { ) {
const newFieldValueUnknown = newFieldValue as unknown; const newFieldValueUnknown = newFieldValue as unknown;

View File

@ -1,8 +1,8 @@
import { createContext } from 'react'; import { createContext } from 'react';
import { FieldDefinition } from '../types/FieldDefinition'; import { FieldDefinition } from '../types/FieldDefinition';
import { ViewFieldMetadata } from '../types/ViewField'; import { FieldMetadata } from '../types/FieldMetadata';
export const EditableFieldDefinitionContext = createContext< export const EditableFieldDefinitionContext = createContext<
FieldDefinition<ViewFieldMetadata> FieldDefinition<FieldMetadata>
>({} as FieldDefinition<ViewFieldMetadata>); >({} as FieldDefinition<FieldMetadata>);

View File

@ -1,9 +1,9 @@
import { FieldMetadata } from './FieldMetadata'; import { FieldMetadata, FieldType } from './FieldMetadata';
export type FieldDefinition<T extends FieldMetadata | unknown> = { export type FieldDefinition<T extends FieldMetadata | unknown> = {
id: string; id: string;
label: string; label: string;
icon?: JSX.Element; icon?: JSX.Element;
type: string; type: FieldType;
metadata: T; metadata: T;
}; };

View File

@ -2,6 +2,7 @@ import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelec
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
export type FieldType = export type FieldType =
| 'unknown'
| 'text' | 'text'
| 'relation' | 'relation'
| 'chip' | 'chip'
@ -14,41 +15,36 @@ export type FieldType =
| 'probability'; | 'probability';
export type FieldTextMetadata = { export type FieldTextMetadata = {
type: 'text';
placeHolder: string; placeHolder: string;
fieldName: string; fieldName: string;
}; };
export type FieldPhoneMetadata = { export type FieldPhoneMetadata = {
type: 'phone';
placeHolder: string; placeHolder: string;
fieldName: string; fieldName: string;
}; };
export type FieldURLMetadata = { export type FieldURLMetadata = {
type: 'url';
placeHolder: string; placeHolder: string;
fieldName: string; fieldName: string;
}; };
export type FieldDateMetadata = { export type FieldDateMetadata = {
type: 'date';
fieldName: string; fieldName: string;
}; };
export type FieldNumberMetadata = { export type FieldNumberMetadata = {
type: 'number';
fieldName: string; fieldName: string;
placeHolder: string;
}; };
export type FieldRelationMetadata = { export type FieldRelationMetadata = {
type: 'relation';
relationType: Entity; relationType: Entity;
fieldName: string; fieldName: string;
useEditButton?: boolean;
}; };
export type FieldChipMetadata = { export type FieldChipMetadata = {
type: 'chip';
relationType: Entity; relationType: Entity;
contentFieldName: string; contentFieldName: string;
urlFieldName: string; urlFieldName: string;
@ -56,7 +52,6 @@ export type FieldChipMetadata = {
}; };
export type FieldDoubleTextMetadata = { export type FieldDoubleTextMetadata = {
type: 'double-text';
firstValueFieldName: string; firstValueFieldName: string;
firstValuePlaceholder: string; firstValuePlaceholder: string;
secondValueFieldName: string; secondValueFieldName: string;
@ -64,7 +59,6 @@ export type FieldDoubleTextMetadata = {
}; };
export type FieldDoubleTextChipMetadata = { export type FieldDoubleTextChipMetadata = {
type: 'double-text-chip';
firstValueFieldName: string; firstValueFieldName: string;
firstValuePlaceholder: string; firstValuePlaceholder: string;
secondValueFieldName: string; secondValueFieldName: string;
@ -74,11 +68,10 @@ export type FieldDoubleTextChipMetadata = {
}; };
export type FieldProbabilityMetadata = { export type FieldProbabilityMetadata = {
type: 'probability';
fieldName: string; fieldName: string;
}; };
export type FieldMetadata = { type: FieldType } & ( export type FieldMetadata =
| FieldTextMetadata | FieldTextMetadata
| FieldRelationMetadata | FieldRelationMetadata
| FieldChipMetadata | FieldChipMetadata
@ -88,8 +81,7 @@ export type FieldMetadata = { type: FieldType } & (
| FieldURLMetadata | FieldURLMetadata
| FieldNumberMetadata | FieldNumberMetadata
| FieldDateMetadata | FieldDateMetadata
| FieldProbabilityMetadata | FieldProbabilityMetadata;
);
export type FieldTextValue = string; export type FieldTextValue = string;

View File

@ -46,6 +46,7 @@ export type ViewFieldRelationMetadata = {
type: 'relation'; type: 'relation';
relationType: Entity; relationType: Entity;
fieldName: string; fieldName: string;
useEditButton?: boolean;
}; };
export type ViewFieldChipMetadata = { export type ViewFieldChipMetadata = {

View File

@ -3,6 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/testing-library'; import { within } from '@storybook/testing-library';
import { graphql } from 'msw'; import { graphql } from 'msw';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -16,7 +17,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/CreateProfile', title: 'Pages/Auth/CreateProfile',
component: CreateProfile, component: CreateProfile,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/create/profile' }, args: { routePath: AppPath.CreateProfile },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: [ msw: [

View File

@ -3,6 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/testing-library'; import { within } from '@storybook/testing-library';
import { graphql } from 'msw'; import { graphql } from 'msw';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -16,7 +17,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/CreateWorkspace', title: 'Pages/Auth/CreateWorkspace',
component: CreateWorkspace, component: CreateWorkspace,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/create/workspace' }, args: { routePath: AppPath.CreateWorkspace },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: [ msw: [

View File

@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { fireEvent, within } from '@storybook/testing-library'; import { fireEvent, within } from '@storybook/testing-library';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -13,7 +14,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/SignInUp', title: 'Pages/Auth/SignInUp',
component: SignInUp, component: SignInUp,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/sign-in' }, args: { routePath: AppPath.SignIn },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -3,25 +3,29 @@ import { useTheme } from '@emotion/react';
import { Timeline } from '@/activities/timeline/components/Timeline'; import { Timeline } from '@/activities/timeline/components/Timeline';
import { CompanyTeam } from '@/companies/components/CompanyTeam'; import { CompanyTeam } from '@/companies/components/CompanyTeam';
import { CompanyAccountOwnerEditableField } from '@/companies/editable-field/components/CompanyAccountOwnerEditableField';
import { CompanyAddressEditableField } from '@/companies/editable-field/components/CompanyAddressEditableField';
import { CompanyCreatedAtEditableField } from '@/companies/editable-field/components/CompanyCreatedAtEditableField';
import { CompanyDomainNameEditableField } from '@/companies/editable-field/components/CompanyDomainNameEditableField';
import { CompanyEmployeesEditableField } from '@/companies/editable-field/components/CompanyEmployeesEditableField';
import { useCompanyQuery } from '@/companies/queries'; import { useCompanyQuery } from '@/companies/queries';
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
import { GenericEditableField } from '@/ui/editable-field/components/GenericEditableField';
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox'; import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
import { EditableFieldDefinitionContext } from '@/ui/editable-field/states/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '@/ui/editable-field/states/EditableFieldEntityIdContext';
import { EditableFieldMutationContext } from '@/ui/editable-field/states/EditableFieldMutationContext';
import { IconBuildingSkyscraper } from '@/ui/icon'; import { IconBuildingSkyscraper } from '@/ui/icon';
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer'; import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer'; import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer'; import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard'; import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { CommentableType } from '~/generated/graphql'; import {
CommentableType,
useUpdateOneCompanyMutation,
} from '~/generated/graphql';
import { getLogoUrlFromDomainName } from '~/utils'; import { getLogoUrlFromDomainName } from '~/utils';
import { CompanyNameEditableField } from '../../modules/companies/editable-field/components/CompanyNameEditableField'; import { CompanyNameEditableField } from '../../modules/companies/editable-field/components/CompanyNameEditableField';
import { ShowPageContainer } from '../../modules/ui/layout/components/ShowPageContainer'; import { ShowPageContainer } from '../../modules/ui/layout/components/ShowPageContainer';
import { companyShowFieldsDefinition } from './constants/companyShowFieldsDefinition';
export function CompanyShow() { export function CompanyShow() {
const companyId = useParams().companyId ?? ''; const companyId = useParams().companyId ?? '';
const { insertCompanyFavorite, deleteCompanyFavorite } = useFavorites(); const { insertCompanyFavorite, deleteCompanyFavorite } = useFavorites();
@ -59,11 +63,22 @@ export function CompanyShow() {
)} )}
/> />
<PropertyBox extraPadding={true}> <PropertyBox extraPadding={true}>
<CompanyDomainNameEditableField company={company} /> <EditableFieldMutationContext.Provider
<CompanyAccountOwnerEditableField company={company} /> value={useUpdateOneCompanyMutation}
<CompanyEmployeesEditableField company={company} /> >
<CompanyAddressEditableField company={company} /> <EditableFieldEntityIdContext.Provider value={company.id}>
<CompanyCreatedAtEditableField company={company} /> {companyShowFieldsDefinition.map((fieldDefinition) => {
return (
<EditableFieldDefinitionContext.Provider
value={fieldDefinition}
key={fieldDefinition.id}
>
<GenericEditableField />
</EditableFieldDefinitionContext.Provider>
);
})}
</EditableFieldEntityIdContext.Provider>
</EditableFieldMutationContext.Provider>
</PropertyBox> </PropertyBox>
<CompanyTeam company={company}></CompanyTeam> <CompanyTeam company={company}></CompanyTeam>
</ShowPageLeftContainer> </ShowPageLeftContainer>

View File

@ -3,6 +3,7 @@ import type { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library'; import { userEvent, within } from '@storybook/testing-library';
import assert from 'assert'; import assert from 'assert';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -18,7 +19,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Companies/FilterBy', title: 'Pages/Companies/FilterBy',
component: Companies, component: Companies,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/companies' }, args: { routePath: AppPath.CompaniesPage },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -2,6 +2,7 @@ import { expect } from '@storybook/jest';
import type { Meta } from '@storybook/react'; import type { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library'; import { userEvent, within } from '@storybook/testing-library';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -16,7 +17,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Companies/SortBy', title: 'Pages/Companies/SortBy',
component: Companies, component: Companies,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/companies' }, args: { routePath: AppPath.CompaniesPage },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -12,7 +13,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Companies', title: 'Pages/Companies',
component: Companies, component: Companies,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/companies' }, args: { routePath: AppPath.CompaniesPage },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -7,6 +7,7 @@ import { graphql } from 'msw';
import { GET_ACTIVITIES_BY_TARGETS, GET_ACTIVITY } from '@/activities/queries'; import { GET_ACTIVITIES_BY_TARGETS, GET_ACTIVITY } from '@/activities/queries';
import { CREATE_ACTIVITY_WITH_COMMENT } from '@/activities/queries/create'; import { CREATE_ACTIVITY_WITH_COMMENT } from '@/activities/queries/create';
import { GET_COMPANY, UPDATE_ONE_COMPANY } from '@/companies/queries'; import { GET_COMPANY, UPDATE_ONE_COMPANY } from '@/companies/queries';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -21,7 +22,10 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Companies/Company', title: 'Pages/Companies/Company',
component: CompanyShow, component: CompanyShow,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/companies/89bb825c-171e-4bcc-9cf7-43448d6fb278' }, args: {
routePath: AppPath.CompanyShowPage,
routeParams: { ':companyId': mockedCompaniesData[0].id },
},
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: [ msw: [

View File

@ -0,0 +1,69 @@
import { FieldDefinition } from '@/ui/editable-field/types/FieldDefinition';
import {
FieldDateMetadata,
FieldMetadata,
FieldNumberMetadata,
FieldRelationMetadata,
FieldTextMetadata,
FieldURLMetadata,
} from '@/ui/editable-field/types/FieldMetadata';
import {
IconCalendar,
IconLink,
IconMap,
IconUserCircle,
IconUsers,
} from '@/ui/icon';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
export const companyShowFieldsDefinition: FieldDefinition<FieldMetadata>[] = [
{
id: 'domainName',
label: 'Domain name',
icon: <IconLink />,
type: 'url',
metadata: {
fieldName: 'domainName',
placeHolder: 'URL',
},
} satisfies FieldDefinition<FieldURLMetadata>,
{
id: 'accountOwner',
label: 'Account owner',
icon: <IconUserCircle />,
type: 'relation',
metadata: {
fieldName: 'accountOwner',
relationType: Entity.User,
},
} satisfies FieldDefinition<FieldRelationMetadata>,
{
id: 'employees',
label: 'Employees',
icon: <IconUsers />,
type: 'number',
metadata: {
fieldName: 'employees',
placeHolder: 'Employees',
},
} satisfies FieldDefinition<FieldNumberMetadata>,
{
id: 'address',
label: 'Address',
icon: <IconMap />,
type: 'text',
metadata: {
fieldName: 'address',
placeHolder: 'Address',
},
} satisfies FieldDefinition<FieldTextMetadata>,
{
id: 'createdAt',
label: 'Created at',
icon: <IconCalendar />,
type: 'date',
metadata: {
fieldName: 'createdAt',
},
} satisfies FieldDefinition<FieldDateMetadata>,
];

View File

@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/testing-library'; import { within } from '@storybook/testing-library';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -13,7 +14,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Opportunities/Default', title: 'Pages/Opportunities/Default',
component: Opportunities, component: Opportunities,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/opportunities' }, args: { routePath: AppPath.OpportunitiesPage },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -21,6 +21,7 @@ import { ShowPageContainer } from '../../modules/ui/layout/components/ShowPageCo
export function PersonShow() { export function PersonShow() {
const personId = useParams().personId ?? ''; const personId = useParams().personId ?? '';
const { insertPersonFavorite, deletePersonFavorite } = useFavorites(); const { insertPersonFavorite, deletePersonFavorite } = useFavorites();
const { data } = usePersonQuery(personId); const { data } = usePersonQuery(personId);

View File

@ -3,6 +3,7 @@ import type { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library'; import { userEvent, within } from '@storybook/testing-library';
import assert from 'assert'; import assert from 'assert';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -18,7 +19,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/People/FilterBy', title: 'Pages/People/FilterBy',
component: People, component: People,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/people' }, args: { routePath: AppPath.PeoplePage },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -6,6 +6,7 @@ import { graphql } from 'msw';
import { UPDATE_ONE_PERSON } from '@/people/queries'; import { UPDATE_ONE_PERSON } from '@/people/queries';
import { SEARCH_COMPANY_QUERY } from '@/search/queries/search'; import { SEARCH_COMPANY_QUERY } from '@/search/queries/search';
import { AppPath } from '@/types/AppPath';
import { Company } from '~/generated/graphql'; import { Company } from '~/generated/graphql';
import { import {
PageDecorator, PageDecorator,
@ -25,7 +26,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/People/Input', title: 'Pages/People/Input',
component: People, component: People,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/people' }, args: { routePath: AppPath.PeoplePage },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,
@ -216,14 +217,6 @@ export const EditRelation: Story = {
await userEvent.click(airbnbChip); await userEvent.click(airbnbChip);
}); });
await step(
'Click on last row company cell to exit relation picker',
async () => {
const otherCell = await canvas.findByText('Janice Dane');
await userEvent.click(otherCell);
},
);
await step('Check if Airbnb is in second row company cell', async () => { await step('Check if Airbnb is in second row company cell', async () => {
await canvas.findByText('Airbnb'); await canvas.findByText('Airbnb');
}); });

View File

@ -2,6 +2,7 @@ import { expect } from '@storybook/jest';
import type { Meta } from '@storybook/react'; import type { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library'; import { userEvent, within } from '@storybook/testing-library';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -17,7 +18,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/People/SortBy', title: 'Pages/People/SortBy',
component: People, component: People,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/people' }, args: { routePath: AppPath.PeoplePage },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -12,7 +13,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/People', title: 'Pages/People',
component: People, component: People,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/people' }, args: { routePath: AppPath.PeoplePage },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -1,6 +1,6 @@
import { Route, Routes } from 'react-router-dom';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -13,15 +13,11 @@ import { PersonShow } from '../PersonShow';
const meta: Meta<PageDecoratorArgs> = { const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/People/Person', title: 'Pages/People/Person',
component: PersonShow, component: PersonShow,
decorators: [ decorators: [PageDecorator],
(Story) => ( args: {
<Routes> routePath: AppPath.PersonShowPage,
<Route path="/person/:personId" element={<Story />} /> routeParams: { ':personId': mockedPeopleData[0].id },
</Routes> },
),
PageDecorator,
],
args: { currentPath: `/person/${mockedPeopleData[0].id}` },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -13,7 +13,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/SettingsProfile', title: 'Pages/Settings/SettingsProfile',
component: SettingsProfile, component: SettingsProfile,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/settings/profile' }, args: { routePath: '/settings/profile' },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -13,7 +13,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/SettingsWorkspaceMembers', title: 'Pages/Settings/SettingsWorkspaceMembers',
component: SettingsWorkspaceMembers, component: SettingsWorkspaceMembers,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/settings/workspace-members' }, args: { routePath: '/settings/workspace-members' },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { AppPath } from '@/types/AppPath';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
@ -12,7 +13,7 @@ const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Tasks/Default', title: 'Pages/Tasks/Default',
component: Tasks, component: Tasks,
decorators: [PageDecorator], decorators: [PageDecorator],
args: { currentPath: '/tasks' }, args: { routePath: AppPath.TasksPage },
parameters: { parameters: {
docs: { story: 'inline', iframeHeight: '500px' }, docs: { story: 'inline', iframeHeight: '500px' },
msw: graphqlMocks, msw: graphqlMocks,

View File

@ -1,4 +1,4 @@
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { Decorator } from '@storybook/react'; import { Decorator } from '@storybook/react';
import { ClientConfigProvider } from '~/modules/client-config/components/ClientConfigProvider'; import { ClientConfigProvider } from '~/modules/client-config/components/ClientConfigProvider';
@ -7,18 +7,32 @@ import { UserProvider } from '~/modules/users/components/UserProvider';
import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout'; import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout';
export type PageDecoratorArgs = { currentPath: string }; export type PageDecoratorArgs = { routePath: string; routeParams: RouteParams };
export const PageDecorator: Decorator<{ currentPath: string }> = ( type RouteParams = {
Story, [param: string]: string;
{ args }, };
) => (
function computeLocation(routePath: string, routeParams: RouteParams) {
return routePath.replace(/:(\w+)/g, (paramName) => {
return routeParams[paramName] ?? '';
});
}
export const PageDecorator: Decorator<{
routePath: string;
routeParams: RouteParams;
}> = (Story, { args }) => (
<UserProvider> <UserProvider>
<ClientConfigProvider> <ClientConfigProvider>
<MemoryRouter initialEntries={[args.currentPath]}> <MemoryRouter
initialEntries={[computeLocation(args.routePath, args.routeParams)]}
>
<FullHeightStorybookLayout> <FullHeightStorybookLayout>
<DefaultLayout> <DefaultLayout>
<Story /> <Routes>
<Route path={args.routePath} element={<Story />} />
</Routes>
</DefaultLayout> </DefaultLayout>
</FullHeightStorybookLayout> </FullHeightStorybookLayout>
</MemoryRouter> </MemoryRouter>