Feat/generic editable board card (#1089)

* Fixed BoardColumnMenu

* Fixed naming

* Optimized board loading

* Added GenericEditableField

* Introduce GenericEditableField for BoardCards

* remove logs

* delete unused files

* fix stories

---------

Co-authored-by: corentin <corentin@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-08-09 05:08:37 +02:00
committed by GitHub
parent 77d356f78a
commit 3666980ccc
103 changed files with 1551 additions and 922 deletions

View File

@ -23,6 +23,7 @@
"apollo-upload-client": "^17.0.0",
"cmdk": "^0.2.0",
"date-fns": "^2.30.0",
"deep-equal": "^2.2.2",
"framer-motion": "^10.12.17",
"graphql": "^16.6.0",
"hex-rgb": "^5.0.0",
@ -124,6 +125,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/apollo-upload-client": "^17.0.2",
"@types/deep-equal": "^1.0.1",
"@types/jest": "^27.5.2",
"@types/js-cookie": "^3.0.3",
"@types/lodash.debounce": "^4.0.7",

View File

@ -1,4 +1,4 @@
import { useLocation } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
@ -17,11 +17,15 @@ import MainNavbar from '@/ui/navbar/components/MainNavbar';
import NavItem from '@/ui/navbar/components/NavItem';
import NavTitle from '@/ui/navbar/components/NavTitle';
import { measureTotalFrameLoad } from './utils/measureTotalFrameLoad';
export function AppNavbar() {
const theme = useTheme();
const currentPath = useLocation().pathname;
const { openCommandMenu } = useCommandMenu();
const navigate = useNavigate();
const isInSubMenu = useIsSubMenuNavbarDisplayed();
return (
@ -62,12 +66,22 @@ export function AppNavbar() {
<NavItem
label="People"
to="/people"
onClick={() => {
measureTotalFrameLoad('people');
navigate('/people');
}}
icon={<IconUser size={theme.icon.size.md} />}
active={currentPath === '/people'}
/>
<NavItem
label="Opportunities"
to="/opportunities"
// to="/opportunities"
onClick={() => {
measureTotalFrameLoad('opportunities');
navigate('/opportunities');
}}
icon={<IconTargetArrow size={theme.icon.size.md} />}
active={currentPath === '/opportunities'}
/>

View File

@ -1942,7 +1942,7 @@ export type User = {
phoneNumber?: Maybe<Scalars['String']>;
settings: UserSettings;
settingsId: Scalars['String'];
supportUserHash: Scalars['String'];
supportUserHash?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
workspaceMember?: Maybe<WorkspaceMember>;
};
@ -2437,7 +2437,7 @@ export type VerifyMutationVariables = Exact<{
}>;
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash: string, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type RenewTokenMutationVariables = Exact<{
refreshToken: Scalars['String'];
@ -2451,7 +2451,7 @@ export type ImpersonateMutationVariables = Exact<{
}>;
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash: string, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
@ -2613,15 +2613,12 @@ export type UpdatePipelineStageMutationVariables = Exact<{
export type UpdatePipelineStageMutation = { __typename?: 'Mutation', updateOnePipelineStage?: { __typename?: 'PipelineStage', id: string, name: string, color: string } | null };
export type UpdateOnePipelineProgressMutationVariables = Exact<{
id?: InputMaybe<Scalars['String']>;
amount?: InputMaybe<Scalars['Int']>;
closeDate?: InputMaybe<Scalars['DateTime']>;
probability?: InputMaybe<Scalars['Int']>;
pointOfContactId?: InputMaybe<Scalars['String']>;
data: PipelineProgressUpdateInput;
where: PipelineProgressWhereUniqueInput;
}>;
export type UpdateOnePipelineProgressMutation = { __typename?: 'Mutation', updateOnePipelineProgress?: { __typename?: 'PipelineProgress', id: string, amount?: number | null, closeDate?: string | null } | null };
export type UpdateOnePipelineProgressMutation = { __typename?: 'Mutation', updateOnePipelineProgress?: { __typename?: 'PipelineProgress', id: string, amount?: number | null, closeDate?: string | null, probability?: number | null } | null };
export type UpdateOnePipelineProgressStageMutationVariables = Exact<{
id?: InputMaybe<Scalars['String']>;
@ -2685,7 +2682,7 @@ export type SearchActivityQuery = { __typename?: 'Query', searchResults: Array<{
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null, canImpersonate: boolean, supportUserHash: string, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, locale: string, colorScheme: ColorScheme } } };
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, locale: string, colorScheme: ColorScheme } } };
export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>;
@ -4564,14 +4561,12 @@ export type UpdatePipelineStageMutationHookResult = ReturnType<typeof useUpdateP
export type UpdatePipelineStageMutationResult = Apollo.MutationResult<UpdatePipelineStageMutation>;
export type UpdatePipelineStageMutationOptions = Apollo.BaseMutationOptions<UpdatePipelineStageMutation, UpdatePipelineStageMutationVariables>;
export const UpdateOnePipelineProgressDocument = gql`
mutation UpdateOnePipelineProgress($id: String, $amount: Int, $closeDate: DateTime, $probability: Int, $pointOfContactId: String) {
updateOnePipelineProgress(
where: {id: $id}
data: {amount: $amount, closeDate: $closeDate, probability: $probability, pointOfContact: {connect: {id: $pointOfContactId}}}
) {
mutation UpdateOnePipelineProgress($data: PipelineProgressUpdateInput!, $where: PipelineProgressWhereUniqueInput!) {
updateOnePipelineProgress(where: $where, data: $data) {
id
amount
closeDate
probability
}
}
`;
@ -4590,11 +4585,8 @@ export type UpdateOnePipelineProgressMutationFn = Apollo.MutationFunction<Update
* @example
* const [updateOnePipelineProgressMutation, { data, loading, error }] = useUpdateOnePipelineProgressMutation({
* variables: {
* id: // value for 'id'
* amount: // value for 'amount'
* closeDate: // value for 'closeDate'
* probability: // value for 'probability'
* pointOfContactId: // value for 'pointOfContactId'
* data: // value for 'data'
* where: // value for 'where'
* },
* });
*/

View File

@ -17,10 +17,7 @@ const meta: Meta<typeof EntityBoard> = {
decorators: [
(Story) => (
<RecoilScope SpecificContext={CompanyBoardContext}>
<HooksCompanyBoard
availableFilters={[]}
orderBy={defaultPipelineProgressOrderBy}
/>
<HooksCompanyBoard orderBy={defaultPipelineProgressOrderBy} />
<MemoryRouter>
<Story />
</MemoryRouter>

View File

@ -19,10 +19,7 @@ const meta: Meta<typeof CompanyBoardCard> = {
decorators: [
(Story) => (
<RecoilScope SpecificContext={CompanyBoardContext}>
<HooksCompanyBoard
availableFilters={[]}
orderBy={defaultPipelineProgressOrderBy}
/>
<HooksCompanyBoard orderBy={defaultPipelineProgressOrderBy} />
<RecoilScope SpecificContext={BoardColumnContext}>
<BoardCardIdContext.Provider value={mockedPipelineProgressData[1].id}>
<MemoryRouter>

View File

@ -1,27 +1,21 @@
import { ReactNode, useCallback, useContext } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { ReactNode, useContext } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState';
import { PipelineProgressPointOfContactEditableField } from '@/pipeline/editable-field/components/PipelineProgressPointOfContactEditableField';
import { ProbabilityEditableField } from '@/pipeline/editable-field/components/ProbabilityEditableField';
import { GET_PIPELINE_PROGRESS, GET_PIPELINES } from '@/pipeline/queries';
import { BoardCardIdContext } from '@/ui/board/states/BoardCardIdContext';
import { fieldsDefinitionsState } from '@/ui/board/states/fieldsDefinitionsState';
import { selectedBoardCardIdsState } from '@/ui/board/states/selectedBoardCardIdsState';
import { EntityChipVariant } from '@/ui/chip/components/EntityChip';
import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField';
import { NumberEditableField } from '@/ui/editable-field/variants/components/NumberEditableField';
import { IconCurrencyDollar, IconProgressCheck } from '@/ui/icon';
import { IconCalendarEvent } from '@/ui/icon';
import { GenericEditableField } from '@/ui/editable-field/components/GenericEditableField';
import {
Checkbox,
CheckboxVariant,
} from '@/ui/input/checkbox/components/Checkbox';
import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext';
import { useUpdateOnePipelineProgressMutation } from '~/generated/graphql';
import { getLogoUrlFromDomainName } from '~/utils';
import { PipelineProgressForBoard } from '../types/CompanyProgress';
import { companyProgressesFamilyState } from '../states/companyProgressesFamilyState';
import { CompanyChip } from './CompanyChip';
@ -106,8 +100,6 @@ const StyledFieldContainer = styled.div`
`;
export function CompanyBoardCard() {
const [updatePipelineProgress] = useUpdateOnePipelineProgressMutation();
const boardCardId = useContext(BoardCardIdContext);
const [companyProgress] = useRecoilState(
@ -118,6 +110,7 @@ export function CompanyBoardCard() {
const [selectedBoardCards, setSelectedBoardCards] = useRecoilState(
selectedBoardCardIdsState,
);
const fieldsDefinitions = useRecoilValue(fieldsDefinitionsState);
const selected = selectedBoardCards.includes(boardCardId ?? '');
@ -131,25 +124,6 @@ export function CompanyBoardCard() {
}
}
const handleCardUpdate = useCallback(
async (pipelineProgress: PipelineProgressForBoard) => {
await updatePipelineProgress({
variables: {
id: pipelineProgress.id,
amount: pipelineProgress.amount,
closeDate: pipelineProgress.closeDate,
probability: pipelineProgress.probability,
pointOfContactId: pipelineProgress.pointOfContactId || undefined,
},
refetchQueries: [
getOperationName(GET_PIPELINE_PROGRESS) ?? '',
getOperationName(GET_PIPELINES) ?? '',
],
});
},
[updatePipelineProgress],
);
if (!company || !pipelineProgress) {
return null;
}
@ -171,71 +145,40 @@ export function CompanyBoardCard() {
}
return (
<StyledBoardCardWrapper>
<StyledBoardCard
selected={selected}
onClick={() => setSelected(!selected)}
>
<StyledBoardCardHeader>
<CompanyChip
id={company.id}
name={company.name}
pictureUrl={getLogoUrlFromDomainName(company.domainName)}
variant={EntityChipVariant.Transparent}
/>
<StyledCheckboxContainer className="checkbox-container">
<Checkbox
checked={selected}
onChange={() => setSelected(!selected)}
variant={CheckboxVariant.Secondary}
<EntityUpdateMutationHookContext.Provider
value={useUpdateOnePipelineProgressMutation}
>
<StyledBoardCardWrapper>
<StyledBoardCard
selected={selected}
onClick={() => setSelected(!selected)}
>
<StyledBoardCardHeader>
<CompanyChip
id={company.id}
name={company.name}
pictureUrl={getLogoUrlFromDomainName(company.domainName)}
variant={EntityChipVariant.Transparent}
/>
</StyledCheckboxContainer>
</StyledBoardCardHeader>
<StyledBoardCardBody>
<PreventSelectOnClickContainer>
<DateEditableField
icon={<IconCalendarEvent />}
value={pipelineProgress.closeDate}
onSubmit={(value) =>
handleCardUpdate({
...pipelineProgress,
closeDate: value,
})
}
/>
</PreventSelectOnClickContainer>
<PreventSelectOnClickContainer>
<NumberEditableField
icon={<IconCurrencyDollar />}
placeholder="Opportunity amount"
value={pipelineProgress.amount}
onSubmit={(value) =>
handleCardUpdate({
...pipelineProgress,
amount: value,
})
}
/>
</PreventSelectOnClickContainer>
<PreventSelectOnClickContainer>
<ProbabilityEditableField
icon={<IconProgressCheck />}
value={pipelineProgress.probability}
onSubmit={(value) => {
handleCardUpdate({
...pipelineProgress,
probability: value,
});
}}
/>
</PreventSelectOnClickContainer>
<PreventSelectOnClickContainer>
<PipelineProgressPointOfContactEditableField
pipelineProgress={pipelineProgress}
/>
</PreventSelectOnClickContainer>
</StyledBoardCardBody>
</StyledBoardCard>
</StyledBoardCardWrapper>
<StyledCheckboxContainer className="checkbox-container">
<Checkbox
checked={selected}
onChange={() => setSelected(!selected)}
variant={CheckboxVariant.Secondary}
/>
</StyledCheckboxContainer>
</StyledBoardCardHeader>
<StyledBoardCardBody>
{fieldsDefinitions.map((viewField) => {
return (
<PreventSelectOnClickContainer key={viewField.id}>
<GenericEditableField viewField={viewField} />
</PreventSelectOnClickContainer>
);
})}
</StyledBoardCardBody>
</StyledBoardCard>
</StyledBoardCardWrapper>
</EntityUpdateMutationHookContext.Provider>
);
}

View File

@ -1,24 +1,13 @@
import { useEffect, useMemo } from 'react';
import { useRecoilCallback, useRecoilState } from 'recoil';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { useInitializeCompanyBoardFilters } from '@/companies/hooks/useInitializeCompanyBoardFilters';
import { companyProgressesFamilyState } from '@/companies/states/companyProgressesFamilyState';
import {
CompanyForBoard,
CompanyProgress,
PipelineProgressForBoard,
} from '@/companies/types/CompanyProgress';
import { currentPipelineState } from '@/pipeline/states/currentPipelineState';
import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardIdsByColumnIdFamilyState';
import { boardColumnsState } from '@/ui/board/states/boardColumnsState';
import { pipelineViewFields } from '@/pipeline/constants/pipelineViewFields';
import { fieldsDefinitionsState } from '@/ui/board/states/fieldsDefinitionsState';
import { isBoardLoadedState } from '@/ui/board/states/isBoardLoadedState';
import { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import {
GetPipelineProgressQuery,
PipelineProgressableType,
PipelineProgressOrderByWithRelationInput as PipelineProgresses_Order_By,
} from '~/generated/graphql';
@ -29,83 +18,44 @@ import {
useGetPipelinesQuery,
} from '~/generated/graphql';
import { useUpdateCompanyBoardCardIds } from '../hooks/useUpdateBoardCardIds';
import { useUpdateCompanyBoard } from '../hooks/useUpdateCompanyBoardColumns';
import { CompanyBoardContext } from '../states/CompanyBoardContext';
export function HooksCompanyBoard({
availableFilters,
orderBy,
}: {
availableFilters: FilterDefinition[];
orderBy: PipelineProgresses_Order_By[];
}) {
useInitializeCompanyBoardFilters({
availableFilters,
});
const setFieldsDefinitionsState = useSetRecoilState(fieldsDefinitionsState);
const [currentPipeline] = useRecoilState(currentPipelineState);
const [, setBoardColumns] = useRecoilState(boardColumnsState);
useEffect(() => {
setFieldsDefinitionsState(pipelineViewFields);
});
const [, setIsBoardLoaded] = useRecoilState(isBoardLoadedState);
const updateBoardColumns = useRecoilCallback(
({ set, snapshot }) =>
(pipeline: Pipeline) => {
const currentPipeline = snapshot
.getLoadable(currentPipelineState)
.valueOrThrow();
const filters = useRecoilScopedValue(filtersScopedState, CompanyBoardContext);
const currentBoardColumns = snapshot
.getLoadable(boardColumnsState)
.valueOrThrow();
const updateCompanyBoard = useUpdateCompanyBoard();
if (JSON.stringify(pipeline) !== JSON.stringify(currentPipeline)) {
set(currentPipelineState, pipeline);
}
const pipelineStages = pipeline?.pipelineStages ?? [];
const orderedPipelineStages = [...pipelineStages].sort((a, b) => {
if (!a.index || !b.index) return 0;
return a.index - b.index;
});
const newBoardColumns: BoardColumnDefinition[] =
orderedPipelineStages?.map((pipelineStage) => ({
id: pipelineStage.id,
title: pipelineStage.name,
colorCode: pipelineStage.color,
index: pipelineStage.index ?? 0,
}));
if (
JSON.stringify(currentBoardColumns) !==
JSON.stringify(newBoardColumns)
) {
setBoardColumns(newBoardColumns);
}
const { data: pipelineData, loading: loadingGetPipelines } =
useGetPipelinesQuery({
variables: {
where: {
pipelineProgressableType: {
equals: PipelineProgressableType.Company,
},
},
},
[],
);
});
useGetPipelinesQuery({
variables: {
where: {
pipelineProgressableType: { equals: PipelineProgressableType.Company },
},
},
onCompleted: async (data) => {
const pipeline = data?.findManyPipeline[0] as Pipeline;
const pipeline = pipelineData?.findManyPipeline[0] as Pipeline | undefined;
updateBoardColumns(pipeline);
},
});
const pipelineStageIds = currentPipeline?.pipelineStages
const pipelineStageIds = pipeline?.pipelineStages
?.map((pipelineStage) => pipelineStage.id)
.flat();
const filters = useRecoilScopedValue(filtersScopedState, CompanyBoardContext);
const whereFilters = useMemo(() => {
return {
AND: [
@ -115,114 +65,52 @@ export function HooksCompanyBoard({
};
}, [filters, pipelineStageIds]) as any;
const updateBoardCardIds = useRecoilCallback(
({ snapshot, set }) =>
(
pipelineProgresses: GetPipelineProgressQuery['findManyPipelineProgress'],
) => {
const boardColumns = snapshot
.getLoadable(boardColumnsState)
.valueOrThrow();
const updateCompanyBoardCardIds = useUpdateCompanyBoardCardIds();
for (const boardColumn of boardColumns) {
const boardCardIds = pipelineProgresses
.filter(
(pipelineProgressToFilter) =>
pipelineProgressToFilter.pipelineStageId === boardColumn.id,
)
.map((pipelineProgress) => pipelineProgress.id);
set(boardCardIdsByColumnIdFamilyState(boardColumn.id), boardCardIds);
}
const { data: pipelineProgressData, loading: loadingGetPipelineProgress } =
useGetPipelineProgressQuery({
variables: {
where: whereFilters,
orderBy,
},
[],
);
onCompleted: (data) => {
const pipelineProgresses = data?.findManyPipelineProgress || [];
const pipelineProgressesQuery = useGetPipelineProgressQuery({
variables: {
where: whereFilters,
orderBy,
},
onCompleted: (data) => {
const pipelineProgresses = data?.findManyPipelineProgress || [];
updateCompanyBoardCardIds(pipelineProgresses);
updateBoardCardIds(pipelineProgresses);
setIsBoardLoaded(true);
},
});
setIsBoardLoaded(true);
},
});
const pipelineProgresses = useMemo(() => {
return pipelineProgressData?.findManyPipelineProgress || [];
}, [pipelineProgressData]);
const pipelineProgresses =
pipelineProgressesQuery.data?.findManyPipelineProgress || [];
const entitiesQueryResult = useGetCompaniesQuery({
variables: {
where: {
id: {
in: pipelineProgresses.map((item) => item.companyId || ''),
const { data: companiesData, loading: loadingGetCompanies } =
useGetCompaniesQuery({
variables: {
where: {
id: {
in: pipelineProgresses.map((item) => item.companyId || ''),
},
},
},
},
});
const indexCompanyByIdReducer = (
acc: { [key: string]: CompanyForBoard },
company: CompanyForBoard,
) => ({
...acc,
[company.id]: company,
});
const companiesDict =
entitiesQueryResult.data?.companies.reduce(
indexCompanyByIdReducer,
{} as { [key: string]: CompanyForBoard },
) || {};
const indexPipelineProgressByIdReducer = (
acc: {
[key: string]: CompanyProgress;
},
pipelineProgress: PipelineProgressForBoard,
) => {
const company =
pipelineProgress.companyId && companiesDict[pipelineProgress.companyId];
if (!company) return acc;
return {
...acc,
[pipelineProgress.id]: {
pipelineProgress,
company,
},
};
};
const companyBoardIndex = pipelineProgresses.reduce(
indexPipelineProgressByIdReducer,
{} as { [key: string]: CompanyProgress },
);
const synchronizeCompanyProgresses = useRecoilCallback(
({ snapshot, set }) =>
(companyBoardIndex: { [key: string]: CompanyProgress }) => {
Object.entries(companyBoardIndex).forEach(([id, companyProgress]) => {
if (
JSON.stringify(
snapshot.getLoadable(companyProgressesFamilyState(id)).getValue(),
) !== JSON.stringify(companyProgress)
) {
set(companyProgressesFamilyState(id), companyProgress);
}
});
},
[],
);
});
const loading =
entitiesQueryResult.loading || pipelineProgressesQuery.loading;
loadingGetPipelines || loadingGetPipelineProgress || loadingGetCompanies;
useEffect(() => {
!loading && synchronizeCompanyProgresses(companyBoardIndex);
}, [loading, companyBoardIndex, synchronizeCompanyProgresses]);
if (!loading && pipeline && pipelineProgresses && companiesData) {
updateCompanyBoard(pipeline, pipelineProgresses, companiesData.companies);
}
}, [
loading,
pipeline,
pipelineProgresses,
companiesData,
updateCompanyBoard,
]);
return <></>;
}

View File

@ -1,3 +1,13 @@
import {
ViewFieldChipMetadata,
ViewFieldDateMetadata,
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldNumberMetadata,
ViewFieldRelationMetadata,
ViewFieldTextMetadata,
ViewFieldURLMetadata,
} from '@/ui/editable-field/types/ViewField';
import {
IconBrandLinkedin,
IconBuildingSkyscraper,
@ -8,16 +18,6 @@ import {
IconUsers,
} from '@/ui/icon/index';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import {
ViewFieldChipMetadata,
ViewFieldDateMetadata,
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldNumberMetadata,
ViewFieldRelationMetadata,
ViewFieldTextMetadata,
ViewFieldURLMetadata,
} from '@/ui/table/types/ViewField';
export const companyViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
{

View File

@ -0,0 +1,30 @@
import { useRecoilCallback } from 'recoil';
import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardIdsByColumnIdFamilyState';
import { boardColumnsState } from '@/ui/board/states/boardColumnsState';
import { GetPipelineProgressQuery } from '~/generated/graphql';
export function useUpdateCompanyBoardCardIds() {
return useRecoilCallback(
({ snapshot, set }) =>
(
pipelineProgresses: GetPipelineProgressQuery['findManyPipelineProgress'],
) => {
const boardColumns = snapshot
.getLoadable(boardColumnsState)
.valueOrThrow();
for (const boardColumn of boardColumns) {
const boardCardIds = pipelineProgresses
.filter(
(pipelineProgressToFilter) =>
pipelineProgressToFilter.pipelineStageId === boardColumn.id,
)
.map((pipelineProgress) => pipelineProgress.id);
set(boardCardIdsByColumnIdFamilyState(boardColumn.id), boardCardIds);
}
},
[],
);
}

View File

@ -0,0 +1,133 @@
import { useRecoilCallback } from 'recoil';
import { currentPipelineState } from '@/pipeline/states/currentPipelineState';
import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardIdsByColumnIdFamilyState';
import { boardColumnsState } from '@/ui/board/states/boardColumnsState';
import { BoardColumnDefinition } from '@/ui/board/types/BoardColumnDefinition';
import { genericEntitiesFamilyState } from '@/ui/editable-field/states/genericEntitiesFamilyState';
import { Pipeline } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { companyProgressesFamilyState } from '../states/companyProgressesFamilyState';
import {
CompanyForBoard,
CompanyProgressDict,
PipelineProgressForBoard,
} from '../types/CompanyProgress';
export function useUpdateCompanyBoard() {
return useRecoilCallback(
({ set, snapshot }) =>
(
pipeline: Pipeline,
pipelineProgresses: (PipelineProgressForBoard & {
pipelineStageId: string;
})[],
companies: CompanyForBoard[],
) => {
const indexCompanyByIdReducer = (
acc: { [key: string]: CompanyForBoard },
company: CompanyForBoard,
) => ({
...acc,
[company.id]: company,
});
const companiesDict =
companies.reduce(
indexCompanyByIdReducer,
{} as { [key: string]: CompanyForBoard },
) ?? {};
const indexPipelineProgressByIdReducer = (
acc: CompanyProgressDict,
pipelineProgress: PipelineProgressForBoard,
) => {
const company =
pipelineProgress.companyId &&
companiesDict[pipelineProgress.companyId];
if (!company) return acc;
return {
...acc,
[pipelineProgress.id]: {
pipelineProgress,
company,
},
};
};
const companyBoardIndex = pipelineProgresses.reduce(
indexPipelineProgressByIdReducer,
{} as CompanyProgressDict,
);
for (const [id, companyProgress] of Object.entries(companyBoardIndex)) {
const currentCompanyProgress = snapshot
.getLoadable(companyProgressesFamilyState(id))
.valueOrThrow();
if (!isDeeplyEqual(currentCompanyProgress, companyProgress)) {
set(companyProgressesFamilyState(id), companyProgress);
set(
genericEntitiesFamilyState(id),
companyProgress.pipelineProgress,
);
}
}
const currentPipeline = snapshot
.getLoadable(currentPipelineState)
.valueOrThrow();
const currentBoardColumns = snapshot
.getLoadable(boardColumnsState)
.valueOrThrow();
if (!isDeeplyEqual(pipeline, currentPipeline)) {
set(currentPipelineState, pipeline);
}
const pipelineStages = pipeline?.pipelineStages ?? [];
const orderedPipelineStages = [...pipelineStages].sort((a, b) => {
if (!a.index || !b.index) return 0;
return a.index - b.index;
});
const newBoardColumns: BoardColumnDefinition[] =
orderedPipelineStages?.map((pipelineStage) => ({
id: pipelineStage.id,
title: pipelineStage.name,
colorCode: pipelineStage.color,
index: pipelineStage.index ?? 0,
}));
if (!isDeeplyEqual(currentBoardColumns, newBoardColumns)) {
set(boardColumnsState, newBoardColumns);
}
for (const boardColumn of newBoardColumns) {
const boardCardIds = pipelineProgresses
.filter(
(pipelineProgressToFilter) =>
pipelineProgressToFilter.pipelineStageId === boardColumn.id,
)
.map((pipelineProgress) => pipelineProgress.id);
const currentBoardCardIds = snapshot
.getLoadable(boardCardIdsByColumnIdFamilyState(boardColumn.id))
.valueOrThrow();
if (!isDeeplyEqual(currentBoardCardIds, boardCardIds)) {
set(
boardCardIdsByColumnIdFamilyState(boardColumn.id),
boardCardIds,
);
}
}
},
[],
);
}

View File

@ -6,6 +6,6 @@ export const companyBoardIndexState = atomFamily<
CompanyProgress | undefined,
string
>({
key: 'currentPipelineState',
key: 'companyBoardIndexState',
default: undefined,
});

View File

@ -7,8 +7,8 @@ import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoi
import { useSearchPeopleQuery } from '~/generated/graphql';
export type OwnProps = {
personId: string;
onSubmit: (newPersonId: string | null) => void;
personId: string | null;
onSubmit: (newPersonId: PersonForSelect | null) => void;
onCancel?: () => void;
};
@ -23,7 +23,7 @@ export function PeoplePicker({ personId, onSubmit, onCancel }: OwnProps) {
const people = useFilteredSearchEntityQuery({
queryHook: useSearchPeopleQuery,
selectedIds: [personId],
selectedIds: [personId ?? ''],
searchFilter: searchFilter,
mappingFunction: (person) => ({
entityType: Entity.Person,
@ -39,7 +39,7 @@ export function PeoplePicker({ personId, onSubmit, onCancel }: OwnProps) {
async function handleEntitySelected(
selectedPerson: PersonForSelect | null | undefined,
) {
onSubmit(selectedPerson?.id ?? null);
onSubmit(selectedPerson ?? null);
}
return (

View File

@ -1,3 +1,13 @@
import {
ViewFieldDateMetadata,
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
ViewFieldMetadata,
ViewFieldPhoneMetadata,
ViewFieldRelationMetadata,
ViewFieldTextMetadata,
ViewFieldURLMetadata,
} from '@/ui/editable-field/types/ViewField';
import {
IconBrandLinkedin,
IconBriefcase,
@ -9,16 +19,6 @@ import {
IconUser,
} from '@/ui/icon/index';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import {
ViewFieldDateMetadata,
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
ViewFieldMetadata,
ViewFieldPhoneMetadata,
ViewFieldRelationMetadata,
ViewFieldTextMetadata,
ViewFieldURLMetadata,
} from '@/ui/table/types/ViewField';
export const peopleViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
{

View File

@ -1,94 +0,0 @@
import { getOperationName } from '@apollo/client/utilities';
import { Key } from 'ts-key-enum';
import { useFilteredSearchPeopleQuery } from '@/people/queries';
import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect';
import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { isCreateModeScopedState } from '@/ui/table/editable-cell/states/isCreateModeScopedState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import {
Person,
PipelineProgress,
useUpdateOnePipelineProgressMutation,
} from '~/generated/graphql';
import { GET_PIPELINE_PROGRESS, GET_PIPELINES } from '../queries';
export type OwnProps = {
pipelineProgress: Pick<PipelineProgress, 'id'> & {
pointOfContact?: Pick<Person, 'id'> | null;
};
onSubmit?: () => void;
onCancel?: () => void;
};
export function PipelineProgressPointOfContactPicker({
pipelineProgress,
onSubmit,
onCancel,
}: OwnProps) {
const [, setIsCreating] = useRecoilScopedState(isCreateModeScopedState);
const [searchFilter] = useRecoilScopedState(
relationPickerSearchFilterScopedState,
);
const [updatePipelineProgress] = useUpdateOnePipelineProgressMutation();
const people = useFilteredSearchPeopleQuery({
searchFilter,
selectedIds: pipelineProgress.pointOfContact?.id
? [pipelineProgress.pointOfContact.id]
: [],
});
async function handleEntitySelected(
entity: EntityForSelect | null | undefined,
) {
if (!entity) {
return;
}
await updatePipelineProgress({
variables: {
...pipelineProgress,
pointOfContactId: entity.id,
},
refetchQueries: [
getOperationName(GET_PIPELINE_PROGRESS) ?? '',
getOperationName(GET_PIPELINES) ?? '',
],
});
onSubmit?.();
}
function handleCreate() {
setIsCreating(true);
onSubmit?.();
}
useScopedHotkeys(
Key.Escape,
() => {
onCancel && onCancel();
},
RelationPickerHotkeyScope.RelationPicker,
[],
);
return (
<SingleEntitySelect
onCreate={handleCreate}
onCancel={onCancel}
onEntitySelected={handleEntitySelected}
entities={{
entitiesToSelect: people.entitiesToSelect,
selectedEntity: people.selectedEntities[0],
loading: people.loading,
}}
/>
);
}

View File

@ -0,0 +1,67 @@
import {
ViewFieldDateMetadata,
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldNumberMetadata,
ViewFieldProbabilityMetadata,
ViewFieldRelationMetadata,
} from '@/ui/editable-field/types/ViewField';
import {
IconCalendarEvent,
IconCurrencyDollar,
IconProgressCheck,
IconUser,
} from '@/ui/icon';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
export const pipelineViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
{
id: 'closeDate',
columnLabel: 'Close Date',
columnIcon: <IconCalendarEvent />,
columnSize: 150,
columnOrder: 4,
metadata: {
type: 'date',
fieldName: 'closeDate',
},
isVisible: true,
} satisfies ViewFieldDefinition<ViewFieldDateMetadata>,
{
id: 'amount',
columnLabel: 'Amount',
columnIcon: <IconCurrencyDollar />,
columnSize: 150,
columnOrder: 4,
metadata: {
type: 'number',
fieldName: 'amount',
},
isVisible: true,
} satisfies ViewFieldDefinition<ViewFieldNumberMetadata>,
{
id: 'probability',
columnLabel: 'Probability',
columnIcon: <IconProgressCheck />,
columnSize: 150,
columnOrder: 4,
metadata: {
type: 'probability',
fieldName: 'probability',
},
isVisible: true,
} satisfies ViewFieldDefinition<ViewFieldProbabilityMetadata>,
{
id: 'pointOfContact',
columnLabel: 'Point of Contact',
columnIcon: <IconUser />,
columnSize: 150,
columnOrder: 4,
metadata: {
type: 'relation',
fieldName: 'pointOfContact',
relationType: Entity.Person,
},
isVisible: true,
} satisfies ViewFieldDefinition<ViewFieldRelationMetadata>,
];

View File

@ -1,53 +0,0 @@
import { PersonChip } from '@/people/components/PersonChip';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { IconUser } from '@/ui/icon';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { Person, PipelineProgress } from '~/generated/graphql';
import { PipelineProgressPointOfContactPickerFieldEditMode } from './PipelineProgressPointOfContactPickerFieldEditMode';
type OwnProps = {
pipelineProgress: Pick<PipelineProgress, 'id' | 'pointOfContactId'> & {
pointOfContact?: Pick<Person, 'id' | 'displayName' | 'avatarUrl'> | null;
};
};
export function PipelineProgressPointOfContactEditableField({
pipelineProgress,
}: OwnProps) {
return (
<RecoilScope SpecificContext={FieldContext}>
<RecoilScope>
<EditableField
useEditButton
customEditHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,
}}
iconLabel={<IconUser />}
editModeContent={
<PipelineProgressPointOfContactPickerFieldEditMode
pipelineProgress={pipelineProgress}
/>
}
displayModeContent={
pipelineProgress.pointOfContact ? (
<PersonChip
id={pipelineProgress.pointOfContact.id}
name={pipelineProgress.pointOfContact.displayName}
pictureUrl={
pipelineProgress.pointOfContact.avatarUrl ?? undefined
}
/>
) : (
<></>
)
}
isDisplayModeContentEmpty={!pipelineProgress.pointOfContact}
isDisplayModeFixHeight
/>
</RecoilScope>
</RecoilScope>
);
}

View File

@ -1,50 +0,0 @@
import styled from '@emotion/styled';
import { PipelineProgressPointOfContactPicker } from '@/pipeline/components/PipelineProgressPointOfContactPicker';
import { useEditableField } from '@/ui/editable-field/hooks/useEditableField';
import { Person, PipelineProgress } from '~/generated/graphql';
const PipelineProgressPointOfContactPickerContainer = styled.div`
left: 0px;
position: absolute;
top: -8px;
`;
export type OwnProps = {
pipelineProgress: Pick<PipelineProgress, 'id'> & {
pointOfContact?: Pick<
Person,
'id' | 'firstName' | 'lastName' | 'displayName'
> | null;
};
onSubmit?: () => void;
onCancel?: () => void;
};
export function PipelineProgressPointOfContactPickerFieldEditMode({
pipelineProgress,
onSubmit,
onCancel,
}: OwnProps) {
const { closeEditableField } = useEditableField();
function handleSubmit() {
closeEditableField();
onSubmit?.();
}
function handleCancel() {
closeEditableField();
onCancel?.();
}
return (
<PipelineProgressPointOfContactPickerContainer>
<PipelineProgressPointOfContactPicker
pipelineProgress={pipelineProgress}
onCancel={handleCancel}
onSubmit={handleSubmit}
/>
</PipelineProgressPointOfContactPickerContainer>
);
}

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 { IconCurrencyDollar } from '@/ui/icon';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import {
PipelineProgress,
useUpdateOnePipelineProgressMutation,
} from '~/generated/graphql';
type OwnProps = {
progress: Pick<PipelineProgress, 'id' | 'amount'>;
};
export function PipelineProgressAmountEditableField({ progress }: OwnProps) {
const [internalValue, setInternalValue] = useState(
progress.amount?.toString(),
);
const [updateOnePipelineProgress] = useUpdateOnePipelineProgressMutation();
useEffect(() => {
setInternalValue(progress.amount?.toString());
}, [progress.amount]);
async function handleChange(newValue: string) {
setInternalValue(newValue);
}
async function handleSubmit() {
if (!internalValue) return;
try {
const numberValue = parseInt(internalValue);
if (isNaN(numberValue)) {
throw new Error('Not a number');
}
await updateOnePipelineProgress({
variables: {
id: progress.id,
amount: numberValue,
},
});
setInternalValue(numberValue.toString());
} catch {
handleCancel();
}
}
async function handleCancel() {
setInternalValue(progress.amount?.toString());
}
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
iconLabel={<IconCurrencyDollar />}
editModeContent={
<TextInputEdit
placeholder={'Amount'}
autoFocus
value={internalValue ?? ''}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
}
displayModeContent={internalValue}
/>
</RecoilScope>
);
}

View File

@ -20,24 +20,14 @@ export const UPDATE_PIPELINE_STAGE = gql`
export const UPDATE_PIPELINE_PROGRESS = gql`
mutation UpdateOnePipelineProgress(
$id: String
$amount: Int
$closeDate: DateTime
$probability: Int
$pointOfContactId: String
$data: PipelineProgressUpdateInput!
$where: PipelineProgressWhereUniqueInput!
) {
updateOnePipelineProgress(
where: { id: $id }
data: {
amount: $amount
closeDate: $closeDate
probability: $probability
pointOfContact: { connect: { id: $pointOfContactId } }
}
) {
updateOnePipelineProgress(where: $where, data: $data) {
id
amount
closeDate
probability
}
}
`;

View File

@ -1,10 +1,8 @@
import React from 'react';
import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { Tag } from '@/ui/tag/components/Tag';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
@ -77,7 +75,6 @@ const StyledNumChildren = styled.div`
type OwnProps = {
color?: string;
title: string;
pipelineStageId?: string;
onTitleEdit: (title: string) => void;
onColumnColorEdit: (color: string) => void;
totalAmount?: number;
@ -104,13 +101,6 @@ export function BoardColumn({
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
useScopedHotkeys(
[Key.Escape, Key.Enter],
handleClose,
BoardColumnHotkeyScope.BoardColumn,
[],
);
function handleTitleClick() {
setIsBoardColumnMenuOpen(true);
setHotkeyScopeAndMemorizePreviousScope(BoardColumnHotkeyScope.BoardColumn, {
@ -132,7 +122,7 @@ export function BoardColumn({
</StyledHeader>
{isBoardColumnMenuOpen && (
<BoardColumnMenu
onClose={() => setIsBoardColumnMenuOpen(false)}
onClose={handleClose}
onTitleEdit={onTitleEdit}
onColumnColorEdit={onColumnColorEdit}
title={title}

View File

@ -1,13 +1,17 @@
import { useRef, useState } from 'react';
import styled from '@emotion/styled';
import { IconPencil } from '@tabler/icons-react';
import { Key } from 'ts-key-enum';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { icon } from '@/ui/theme/constants/icon';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { BoardColumnHotkeyScope } from '../types/BoardColumnHotkeyScope';
import { BoardColumnEditTitleMenu } from './BoardColumnEditTitleMenu';
const StyledMenuContainer = styled.div`
@ -39,6 +43,13 @@ export function BoardColumnMenu({
callback: onClose,
});
useScopedHotkeys(
[Key.Escape, Key.Enter],
onClose,
BoardColumnHotkeyScope.BoardColumn,
[],
);
return (
<StyledMenuContainer ref={boardColumnMenuRef}>
<DropdownMenu>

View File

@ -48,6 +48,7 @@ export function EntityBoard({
onEditColumnColor: (columnId: string, color: string) => void;
}) {
const [boardColumns] = useRecoilState(boardColumnsState);
const theme = useTheme();
const [updatePipelineProgressStage] =
useUpdateOnePipelineProgressStageMutation();

View File

@ -4,19 +4,15 @@ import { BoardOptions } from '../types/BoardOptions';
export function EntityBoardCard({
boardOptions,
pipelineProgressId,
cardId,
index,
}: {
boardOptions: BoardOptions;
pipelineProgressId: string;
cardId: string;
index: number;
}) {
return (
<Draggable
key={pipelineProgressId}
draggableId={pipelineProgressId}
index={index}
>
<Draggable key={cardId} draggableId={cardId} index={index}>
{(draggableProvided) => (
<div
ref={draggableProvided?.innerRef}

View File

@ -84,7 +84,6 @@ export function EntityBoardColumn({
onTitleEdit={handleEditColumnTitle}
title={column.title}
color={column.colorCode}
pipelineStageId={column.id}
totalAmount={boardColumnTotal}
isFirstColumn={column.index === 0}
numChildren={cardIds.length}
@ -94,7 +93,7 @@ export function EntityBoardColumn({
<BoardCardIdContext.Provider value={cardId} key={cardId}>
<EntityBoardCard
index={index}
pipelineProgressId={cardId}
cardId={cardId}
boardOptions={boardOptions}
/>
</BoardCardIdContext.Provider>

View File

@ -0,0 +1,9 @@
import { createContext } from 'react';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../../editable-field/types/ViewField';
export const FieldDefinitionContext =
createContext<ViewFieldDefinition<ViewFieldMetadata> | null>(null);

View File

@ -0,0 +1,13 @@
import { atom } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../../editable-field/types/ViewField';
export const fieldsDefinitionsState = atom<
ViewFieldDefinition<ViewFieldMetadata>[]
>({
key: 'fieldsDefinitionState',
default: [],
});

View File

@ -0,0 +1,49 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { BoardCardIdContext } from '@/ui/board/states/BoardCardIdContext';
import {
ViewFieldDateMetadata,
ViewFieldDefinition,
} from '@/ui/editable-field/types/ViewField';
import { DateInputDisplay } from '@/ui/input/date/components/DateInputDisplay';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { parseDate } from '~/utils/date-utils';
import { FieldContext } from '../states/FieldContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { EditableField } from './EditableField';
import { GenericEditableDateFieldEditMode } from './GenericEditableDateFieldEditMode';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldDateMetadata>;
};
export function GenericEditableDateField({ viewField }: OwnProps) {
const currentEntityId = useContext(BoardCardIdContext);
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
const internalDateValue = fieldValue
? parseDate(fieldValue).toJSDate()
: null;
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
iconLabel={viewField.columnIcon}
editModeContent={
<GenericEditableDateFieldEditMode viewField={viewField} />
}
displayModeContent={<DateInputDisplay value={internalDateValue} />}
isDisplayModeContentEmpty={!fieldValue}
/>
</RecoilScope>
);
}

View File

@ -0,0 +1,44 @@
import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { BoardCardIdContext } from '@/ui/board/states/BoardCardIdContext';
import {
ViewFieldDateMetadata,
ViewFieldDefinition,
} from '@/ui/editable-field/types/ViewField';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { EditableFieldEditModeDate } from '../variants/components/EditableFieldEditModeDate';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldDateMetadata>;
};
export function GenericEditableDateFieldEditMode({ viewField }: OwnProps) {
const currentEntityId = useContext(BoardCardIdContext);
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<string>(
genericEntityFieldFamilySelector({
entityId: currentEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
const updateField = useUpdateGenericEntityField();
function handleSubmit(newDateISO: string) {
if (newDateISO === fieldValue || !newDateISO) return;
setFieldValue(newDateISO);
if (currentEntityId && updateField && newDateISO) {
updateField(currentEntityId, viewField, newDateISO);
}
}
return (
<EditableFieldEditModeDate value={fieldValue} onChange={handleSubmit} />
);
}

View File

@ -0,0 +1,35 @@
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { isViewFieldDate } from '../types/guards/isViewFieldDate';
import { isViewFieldNumber } from '../types/guards/isViewFieldNumber';
import { isViewFieldProbability } from '../types/guards/isViewFieldProbability';
import { isViewFieldRelation } from '../types/guards/isViewFieldRelation';
import { GenericEditableDateField } from './GenericEditableDateField';
import { GenericEditableNumberField } from './GenericEditableNumberField';
import { GenericEditableRelationField } from './GenericEditableRelationField';
import { ProbabilityEditableField } from './ProbabilityEditableField';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldMetadata>;
};
export function GenericEditableField({ viewField: fieldDefinition }: OwnProps) {
if (isViewFieldDate(fieldDefinition)) {
return <GenericEditableDateField viewField={fieldDefinition} />;
} else if (isViewFieldNumber(fieldDefinition)) {
return <GenericEditableNumberField viewField={fieldDefinition} />;
} else if (isViewFieldRelation(fieldDefinition)) {
return <GenericEditableRelationField viewField={fieldDefinition} />;
} else if (isViewFieldProbability(fieldDefinition)) {
return <ProbabilityEditableField viewField={fieldDefinition} />;
} else {
console.warn(
`Unknown field metadata type: ${fieldDefinition.metadata.type} in GenericEditableField`,
);
return <></>;
}
}

View File

@ -0,0 +1,43 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { BoardCardIdContext } from '@/ui/board/states/BoardCardIdContext';
import {
ViewFieldDefinition,
ViewFieldNumberMetadata,
} from '@/ui/editable-field/types/ViewField';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { FieldContext } from '../states/FieldContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { EditableField } from './EditableField';
import { GenericEditableNumberFieldEditMode } from './GenericEditableNumberFieldEditMode';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldNumberMetadata>;
};
export function GenericEditableNumberField({ viewField }: OwnProps) {
const currentEntityId = useContext(BoardCardIdContext);
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
iconLabel={viewField.columnIcon}
editModeContent={
<GenericEditableNumberFieldEditMode viewField={viewField} />
}
displayModeContent={fieldValue}
isDisplayModeContentEmpty={!fieldValue}
/>
</RecoilScope>
);
}

View File

@ -0,0 +1,78 @@
import { useContext, useRef, useState } from 'react';
import { useRecoilState } from 'recoil';
import { BoardCardIdContext } from '@/ui/board/states/BoardCardIdContext';
import {
ViewFieldDefinition,
ViewFieldNumberMetadata,
} from '@/ui/editable-field/types/ViewField';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import {
canBeCastAsIntegerOrNull,
castAsIntegerOrNull,
} from '~/utils/cast-as-integer-or-null';
import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldNumberMetadata>;
};
export function GenericEditableNumberFieldEditMode({ viewField }: OwnProps) {
const currentEntityId = useContext(BoardCardIdContext);
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<number | null>(
genericEntityFieldFamilySelector({
entityId: currentEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
const [internalValue, setInternalValue] = useState(
fieldValue ? fieldValue.toString() : '',
);
const updateField = useUpdateGenericEntityField();
function handleSubmit() {
if (!canBeCastAsIntegerOrNull(internalValue)) {
return;
}
if (internalValue === fieldValue) return;
setFieldValue(castAsIntegerOrNull(internalValue));
if (currentEntityId && updateField) {
updateField(
currentEntityId,
viewField,
castAsIntegerOrNull(internalValue),
);
}
}
function onCancel() {
setFieldValue(fieldValue);
}
function handleChange(newValue: string) {
setInternalValue(newValue);
}
const wrapperRef = useRef(null);
useRegisterCloseFieldHandlers(wrapperRef, handleSubmit, onCancel);
return (
<div ref={wrapperRef}>
<TextInputEdit
autoFocus
value={internalValue ? internalValue.toString() : ''}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
</div>
);
}

View File

@ -0,0 +1,80 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { PersonChip } from '@/people/components/PersonChip';
import { BoardCardIdContext } from '@/ui/board/states/BoardCardIdContext';
import {
ViewFieldDefinition,
ViewFieldRelationMetadata,
} from '@/ui/editable-field/types/ViewField';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { FieldContext } from '../states/FieldContext';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import { EditableField } from './EditableField';
import { GenericEditableRelationFieldEditMode } from './GenericEditableRelationFieldEditMode';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldRelationMetadata>;
};
function RelationChip({
fieldDefinition,
fieldValue,
}: {
fieldDefinition: ViewFieldDefinition<ViewFieldRelationMetadata>;
fieldValue: any | null;
}) {
switch (fieldDefinition.metadata.relationType) {
case Entity.Person: {
return (
<PersonChip
id={fieldValue?.id ?? ''}
name={fieldValue?.displayName ?? ''}
pictureUrl={fieldValue?.avatarUrl ?? ''}
/>
);
}
default:
console.warn(
`Unknown relation type: "${fieldDefinition.metadata.relationType}" in GenericEditableRelationField`,
);
return <> </>;
}
}
export function GenericEditableRelationField({ viewField }: OwnProps) {
const currentEntityId = useContext(BoardCardIdContext);
const fieldValue = useRecoilValue<any | null>(
genericEntityFieldFamilySelector({
entityId: currentEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
return (
<RecoilScope SpecificContext={FieldContext}>
<RecoilScope>
<EditableField
useEditButton
customEditHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,
}}
iconLabel={viewField.columnIcon}
editModeContent={
<GenericEditableRelationFieldEditMode viewField={viewField} />
}
displayModeContent={
<RelationChip fieldDefinition={viewField} fieldValue={fieldValue} />
}
isDisplayModeContentEmpty={!fieldValue}
isDisplayModeFixHeight
/>
</RecoilScope>
</RecoilScope>
);
}

View File

@ -0,0 +1,102 @@
import { useContext } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { PeoplePicker } from '@/people/components/PeoplePicker';
import { BoardCardIdContext } from '@/ui/board/states/BoardCardIdContext';
import {
ViewFieldDefinition,
ViewFieldRelationMetadata,
ViewFieldRelationValue,
} from '@/ui/editable-field/types/ViewField';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { useEditableField } from '../hooks/useEditableField';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
const RelationPickerContainer = styled.div`
left: 0px;
position: absolute;
top: -8px;
`;
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldRelationMetadata>;
};
function RelationPicker({
fieldDefinition,
fieldValue,
handleEntitySubmit,
handleCancel,
}: {
fieldDefinition: ViewFieldDefinition<ViewFieldRelationMetadata>;
fieldValue: ViewFieldRelationValue;
handleEntitySubmit: (newRelationId: EntityForSelect | null) => void;
handleCancel: () => void;
}) {
switch (fieldDefinition.metadata.relationType) {
case Entity.Person: {
return (
<PeoplePicker
personId={fieldValue?.id ?? null}
onSubmit={handleEntitySubmit}
onCancel={handleCancel}
/>
);
}
default:
console.warn(
`Unknown relation type: "${fieldDefinition.metadata.relationType}" in GenericEditableRelationField`,
);
return <> </>;
}
}
export function GenericEditableRelationFieldEditMode({ viewField }: OwnProps) {
const currentEntityId = useContext(BoardCardIdContext);
// TODO: we could use a hook that would return the field value with the right type
const [fieldValue, setFieldValue] = useRecoilState<any | null>(
genericEntityFieldFamilySelector({
entityId: currentEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
const updateField = useUpdateGenericEntityField();
const { closeEditableField } = useEditableField();
function handleSubmit(newRelation: EntityForSelect | null) {
if (newRelation?.id === fieldValue?.id) return;
setFieldValue({
id: newRelation?.id ?? null,
displayName: newRelation?.name ?? null,
avatarUrl: newRelation?.avatarUrl ?? null,
});
if (currentEntityId && updateField) {
updateField(currentEntityId, viewField, newRelation);
}
closeEditableField();
}
function handleCancel() {
closeEditableField();
}
return (
<RelationPickerContainer>
<RelationPicker
fieldDefinition={viewField}
fieldValue={fieldValue}
handleEntitySubmit={handleSubmit}
handleCancel={handleCancel}
/>
</RelationPickerContainer>
);
}

View File

@ -1,25 +1,27 @@
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import {
ViewFieldDefinition,
ViewFieldProbabilityMetadata,
} from '@/ui/editable-field/types/ViewField';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { ProbabilityFieldEditMode } from './ProbabilityFieldEditMode';
import { ProbabilityEditableFieldEditMode } from './ProbabilityEditableFieldEditMode';
type OwnProps = {
icon?: React.ReactNode;
value: number | null | undefined;
onSubmit?: (newValue: number) => void;
viewField: ViewFieldDefinition<ViewFieldProbabilityMetadata>;
};
export function ProbabilityEditableField({ icon, value, onSubmit }: OwnProps) {
export function ProbabilityEditableField({ viewField }: OwnProps) {
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
iconLabel={icon}
iconLabel={viewField.columnIcon}
displayModeContent={
<ProbabilityEditableFieldEditMode viewField={viewField} />
}
displayModeContentOnly
disableHoverEffect
displayModeContent={
<ProbabilityFieldEditMode value={value ?? 0} onChange={onSubmit} />
}
/>
</RecoilScope>
);

View File

@ -0,0 +1,136 @@
import { useContext, useState } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { BoardCardIdContext } from '@/ui/board/states/BoardCardIdContext';
import { useEditableField } from '@/ui/editable-field/hooks/useEditableField';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldProbabilityMetadata,
} from '../types/ViewField';
const StyledContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
justify-content: flex-start;
width: 100%;
`;
const StyledProgressBarItemContainer = styled.div`
align-items: center;
display: flex;
height: ${({ theme }) => theme.spacing(4)};
padding-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledProgressBarItem = styled.div<{
isFirst: boolean;
isLast: boolean;
isActive: boolean;
}>`
background-color: ${({ theme, isActive }) =>
isActive
? theme.font.color.secondary
: theme.background.transparent.medium};
border-bottom-left-radius: ${({ theme, isFirst }) =>
isFirst ? theme.border.radius.sm : theme.border.radius.xs};
border-bottom-right-radius: ${({ theme, isLast }) =>
isLast ? theme.border.radius.sm : theme.border.radius.xs};
border-top-left-radius: ${({ theme, isFirst }) =>
isFirst ? theme.border.radius.sm : theme.border.radius.xs};
border-top-right-radius: ${({ theme, isLast }) =>
isLast ? theme.border.radius.sm : theme.border.radius.xs};
height: ${({ theme }) => theme.spacing(2)};
width: ${({ theme }) => theme.spacing(3)};
`;
const StyledProgressBarContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
justify-content: flex-start;
width: 100%;
`;
const StyledLabel = styled.div`
width: ${({ theme }) => theme.spacing(12)};
`;
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldProbabilityMetadata>;
};
const PROBABILITY_VALUES = [
{ label: '0%', value: 0 },
{ label: '25%', value: 25 },
{ label: '50%', value: 50 },
{ label: '75%', value: 75 },
{ label: '100%', value: 100 },
];
export function ProbabilityEditableFieldEditMode({ viewField }: OwnProps) {
const [nextProbabilityIndex, setNextProbabilityIndex] = useState<
number | null
>(null);
const currentEntityId = useContext(BoardCardIdContext);
const [fieldValue, setFieldValue] = useRecoilState<number>(
genericEntityFieldFamilySelector({
entityId: currentEntityId ?? '',
fieldName: viewField.metadata.fieldName,
}),
);
const probabilityIndex = Math.ceil(fieldValue / 25);
const { closeEditableField } = useEditableField();
const updateField = useUpdateGenericEntityField();
function handleChange(newValue: number) {
setFieldValue(newValue);
if (currentEntityId && updateField && newValue) {
updateField(currentEntityId, viewField, newValue);
}
closeEditableField();
}
console.log(probabilityIndex);
return (
<StyledContainer>
<StyledLabel>
{
PROBABILITY_VALUES[
nextProbabilityIndex || nextProbabilityIndex === 0
? nextProbabilityIndex
: probabilityIndex
].label
}
</StyledLabel>
<StyledProgressBarContainer>
{PROBABILITY_VALUES.map((probability, i) => (
<StyledProgressBarItemContainer
key={i}
onClick={() => handleChange(probability.value)}
onMouseEnter={() => setNextProbabilityIndex(i)}
onMouseLeave={() => setNextProbabilityIndex(null)}
>
<StyledProgressBarItem
isActive={
nextProbabilityIndex || nextProbabilityIndex === 0
? i <= nextProbabilityIndex
: i <= probabilityIndex
}
key={probability.label}
isFirst={i === 0}
isLast={i === PROBABILITY_VALUES.length - 1}
/>
</StyledProgressBarItemContainer>
))}
</StyledProgressBarContainer>
</StyledContainer>
);
}

View File

@ -0,0 +1,7 @@
import { useContext } from 'react';
import { EntityIdContext } from '../states/EntityIdContext';
export function useCurrentEntityId() {
return useContext(EntityIdContext);
}

View File

@ -0,0 +1,246 @@
import { useContext } from 'react';
import { isViewFieldChip } from '@/ui/editable-field/types/guards/isViewFieldChip';
import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext';
import { isViewFieldChipValue } from '../types/guards/isViewFieldChipValue';
import { isViewFieldDate } from '../types/guards/isViewFieldDate';
import { isViewFieldDateValue } from '../types/guards/isViewFieldDateValue';
import { isViewFieldDoubleText } from '../types/guards/isViewFieldDoubleText';
import { isViewFieldDoubleTextChip } from '../types/guards/isViewFieldDoubleTextChip';
import { isViewFieldDoubleTextChipValue } from '../types/guards/isViewFieldDoubleTextChipValue';
import { isViewFieldDoubleTextValue } from '../types/guards/isViewFieldDoubleTextValue';
import { isViewFieldNumber } from '../types/guards/isViewFieldNumber';
import { isViewFieldNumberValue } from '../types/guards/isViewFieldNumberValue';
import { isViewFieldPhone } from '../types/guards/isViewFieldPhone';
import { isViewFieldPhoneValue } from '../types/guards/isViewFieldPhoneValue';
import { isViewFieldProbability } from '../types/guards/isViewFieldProbability';
import { isViewFieldProbabilityValue } from '../types/guards/isViewFieldProbabilityValue';
import { isViewFieldRelation } from '../types/guards/isViewFieldRelation';
import { isViewFieldRelationValue } from '../types/guards/isViewFieldRelationValue';
import { isViewFieldText } from '../types/guards/isViewFieldText';
import { isViewFieldTextValue } from '../types/guards/isViewFieldTextValue';
import { isViewFieldURL } from '../types/guards/isViewFieldURL';
import { isViewFieldURLValue } from '../types/guards/isViewFieldURLValue';
import {
ViewFieldChipMetadata,
ViewFieldChipValue,
ViewFieldDateMetadata,
ViewFieldDateValue,
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
ViewFieldDoubleTextChipValue,
ViewFieldDoubleTextMetadata,
ViewFieldDoubleTextValue,
ViewFieldMetadata,
ViewFieldNumberMetadata,
ViewFieldNumberValue,
ViewFieldPhoneMetadata,
ViewFieldPhoneValue,
ViewFieldProbabilityMetadata,
ViewFieldProbabilityValue,
ViewFieldRelationMetadata,
ViewFieldRelationValue,
ViewFieldTextMetadata,
ViewFieldTextValue,
ViewFieldURLMetadata,
ViewFieldURLValue,
} from '../types/ViewField';
export function useUpdateGenericEntityField() {
const useUpdateEntityMutation = useContext(EntityUpdateMutationHookContext);
const [updateEntity] = useUpdateEntityMutation();
return function updatePeopleField<
MetadataType extends ViewFieldMetadata,
ValueType extends MetadataType extends ViewFieldDoubleTextMetadata
? ViewFieldDoubleTextValue
: MetadataType extends ViewFieldTextMetadata
? ViewFieldTextValue
: MetadataType extends ViewFieldPhoneMetadata
? ViewFieldPhoneValue
: MetadataType extends ViewFieldURLMetadata
? ViewFieldURLValue
: MetadataType extends ViewFieldNumberMetadata
? ViewFieldNumberValue
: MetadataType extends ViewFieldDateMetadata
? ViewFieldDateValue
: MetadataType extends ViewFieldChipMetadata
? ViewFieldChipValue
: MetadataType extends ViewFieldDoubleTextChipMetadata
? ViewFieldDoubleTextChipValue
: MetadataType extends ViewFieldRelationMetadata
? ViewFieldRelationValue
: MetadataType extends ViewFieldProbabilityMetadata
? ViewFieldProbabilityValue
: unknown,
>(
currentEntityId: string,
viewField: ViewFieldDefinition<MetadataType>,
newFieldValue: ValueType,
) {
const newFieldValueUnknown = newFieldValue as unknown;
// TODO: improve type guards organization, maybe with a common typeguard for all view fields
// taking an object of options as parameter ?
//
// The goal would be to check that the view field value not only is valid,
// but also that it is validated against the corresponding view field type
// Relation
if (
isViewFieldRelation(viewField) &&
isViewFieldRelationValue(newFieldValueUnknown)
) {
const newSelectedEntity = newFieldValueUnknown;
const fieldName = viewField.metadata.fieldName;
if (!newSelectedEntity) {
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[fieldName]: {
disconnect: true,
},
},
},
});
} else {
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[fieldName]: {
connect: { id: newSelectedEntity.id },
},
},
},
});
}
// Chip
} else if (
isViewFieldChip(viewField) &&
isViewFieldChipValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.contentFieldName]: newContent },
},
});
// Text
} else if (
isViewFieldText(viewField) &&
isViewFieldTextValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
// Double text
} else if (
isViewFieldDoubleText(viewField) &&
isViewFieldDoubleTextValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[viewField.metadata.firstValueFieldName]: newContent.firstValue,
[viewField.metadata.secondValueFieldName]: newContent.secondValue,
},
},
});
// Double Text Chip
} else if (
isViewFieldDoubleTextChip(viewField) &&
isViewFieldDoubleTextChipValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: {
[viewField.metadata.firstValueFieldName]: newContent.firstValue,
[viewField.metadata.secondValueFieldName]: newContent.secondValue,
},
},
});
// Phone
} else if (
isViewFieldPhone(viewField) &&
isViewFieldPhoneValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
// URL
} else if (
isViewFieldURL(viewField) &&
isViewFieldURLValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
// Number
} else if (
isViewFieldNumber(viewField) &&
isViewFieldNumberValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
// Date
} else if (
isViewFieldDate(viewField) &&
isViewFieldDateValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
} else if (
isViewFieldProbability(viewField) &&
isViewFieldProbabilityValue(newFieldValueUnknown)
) {
const newContent = newFieldValueUnknown;
updateEntity({
variables: {
where: { id: currentEntityId },
data: { [viewField.metadata.fieldName]: newContent },
},
});
}
};
}

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const EditableFieldContext = createContext<string | null>(null);

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const EntityIdContext = createContext<string | null>(null);

View File

@ -0,0 +1,9 @@
import { atomFamily } from 'recoil';
export const genericEntitiesFamilyState = atomFamily<
Record<string, unknown> | null,
string
>({
key: 'genericEntitiesFamilyState',
default: null,
});

View File

@ -0,0 +1,18 @@
import { selectorFamily } from 'recoil';
import { genericEntitiesFamilyState } from './genericEntitiesFamilyState';
export const genericEntityFieldFamilySelector = selectorFamily({
key: 'genericEntityFieldFamilySelector',
get:
<T>({ fieldName, entityId }: { fieldName: string; entityId: string }) =>
({ get }) =>
get(genericEntitiesFamilyState(entityId))?.[fieldName] as T,
set:
<T>({ fieldName, entityId }: { fieldName: string; entityId: string }) =>
({ set }, newValue: T) =>
set(genericEntitiesFamilyState(entityId), (prevState) => ({
...prevState,
[fieldName]: newValue,
})),
});

View File

@ -10,7 +10,8 @@ export type ViewFieldType =
| 'number'
| 'date'
| 'phone'
| 'url';
| 'url'
| 'probability';
export type ViewFieldTextMetadata = {
type: 'text';
@ -72,6 +73,11 @@ export type ViewFieldDoubleTextChipMetadata = {
entityType: Entity;
};
export type ViewFieldProbabilityMetadata = {
type: 'probability';
fieldName: string;
};
export type ViewFieldMetadata = { type: ViewFieldType } & (
| ViewFieldTextMetadata
| ViewFieldRelationMetadata
@ -82,6 +88,7 @@ export type ViewFieldMetadata = { type: ViewFieldType } & (
| ViewFieldURLMetadata
| ViewFieldNumberMetadata
| ViewFieldDateMetadata
| ViewFieldProbabilityMetadata
);
export type ViewFieldDefinition<T extends ViewFieldMetadata | unknown> = {
@ -101,7 +108,8 @@ export type ViewFieldChipValue = string;
export type ViewFieldDateValue = string;
export type ViewFieldPhoneValue = string;
export type ViewFieldURLValue = string;
export type ViewFieldNumberValue = number;
export type ViewFieldNumberValue = number | null;
export type ViewFieldProbabilityValue = number;
export type ViewFieldDoubleTextValue = {
firstValue: string;

View File

@ -0,0 +1,11 @@
import {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldProbabilityMetadata,
} from '../ViewField';
export function isViewFieldProbability(
field: ViewFieldDefinition<ViewFieldMetadata>,
): field is ViewFieldDefinition<ViewFieldProbabilityMetadata> {
return field.metadata.type === 'probability';
}

View File

@ -0,0 +1,12 @@
import { ViewFieldProbabilityValue } from '../ViewField';
// TODO: add yup
export function isViewFieldProbabilityValue(
fieldValue: unknown,
): fieldValue is ViewFieldProbabilityValue {
return (
fieldValue !== null &&
fieldValue !== undefined &&
typeof fieldValue === 'number'
);
}

View File

@ -1,5 +1,3 @@
import { useEffect, useState } from 'react';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { DateInputDisplay } from '@/ui/input/date/components/DateInputDisplay';
@ -16,49 +14,29 @@ type OwnProps = {
};
export function DateEditableField({ icon, value, label, onSubmit }: OwnProps) {
const [internalValue, setInternalValue] = useState(value);
useEffect(() => {
setInternalValue(value);
}, [value]);
async function handleChange(newValue: string) {
setInternalValue(newValue);
onSubmit?.(newValue);
}
async function handleSubmit() {
if (!internalValue) return;
onSubmit?.(internalValue);
}
async function handleCancel() {
setInternalValue(value);
}
const internalDateValue = internalValue
? parseDate(internalValue).toJSDate()
: null;
const internalDateValue = value ? parseDate(value).toJSDate() : null;
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
// onSubmit={handleSubmit}
// onCancel={handleCancel}
iconLabel={icon}
label={label}
editModeContent={
<EditableFieldEditModeDate
value={internalValue || new Date().toISOString()}
value={value || new Date().toISOString()}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
}
displayModeContent={<DateInputDisplay value={internalDateValue} />}
isDisplayModeContentEmpty={!internalValue}
isDisplayModeContentEmpty={!value}
/>
</RecoilScope>
);

View File

@ -1,3 +1,5 @@
import { useEffect, useState } from 'react';
import { DateInputEdit } from '@/ui/input/date/components/DateInputEdit';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { parseDate } from '~/utils/date-utils';
@ -11,6 +13,12 @@ type OwnProps = {
};
export function EditableFieldEditModeDate({ value, onChange }: OwnProps) {
const [internalValue, setInternalValue] = useState(value);
useEffect(() => {
setInternalValue(value);
}, [value]);
const { closeEditableField } = useEditableField();
function handleChange(newValue: string) {
@ -20,7 +28,7 @@ export function EditableFieldEditModeDate({ value, onChange }: OwnProps) {
return (
<DateInputEdit
value={parseDate(value).toJSDate()}
value={internalValue ? parseDate(internalValue).toJSDate() : new Date()}
onChange={(newDate: Date) => {
handleChange(newDate.toISOString());
}}

View File

@ -1,73 +0,0 @@
import { useEffect, useState } from 'react';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import {
canBeCastAsIntegerOrNull,
castAsIntegerOrNull,
} from '~/utils/cast-as-integer-or-null';
type OwnProps = {
icon?: React.ReactNode;
placeholder?: string;
value: number | null | undefined;
onSubmit?: (newValue: number | null) => void;
};
export function NumberEditableField({
icon,
placeholder,
value,
onSubmit,
}: OwnProps) {
const [internalValue, setInternalValue] = useState(value?.toString());
useEffect(() => {
setInternalValue(value?.toString());
}, [value]);
async function handleChange(newValue: string) {
setInternalValue(newValue);
}
async function handleSubmit() {
if (!canBeCastAsIntegerOrNull(internalValue)) {
handleCancel();
return;
}
const valueCastedAsNumberOrNull = castAsIntegerOrNull(internalValue);
onSubmit?.(valueCastedAsNumberOrNull);
setInternalValue(valueCastedAsNumberOrNull?.toString());
}
async function handleCancel() {
setInternalValue(value?.toString());
}
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
iconLabel={icon}
editModeContent={
<TextInputEdit
placeholder={placeholder ?? ''}
autoFocus
value={internalValue ?? ''}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
}
displayModeContent={internalValue}
isDisplayModeContentEmpty={!(internalValue !== '' && internalValue)}
/>
</RecoilScope>
);
}

View File

@ -1,32 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconCurrencyDollar } from '@tabler/icons-react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { NumberEditableField } from '../NumberEditableField';
const meta: Meta<typeof NumberEditableField> = {
title: 'UI/EditableField/NumberEditableField',
component: NumberEditableField,
decorators: [ComponentDecorator],
argTypes: {
icon: {
type: 'boolean',
mapping: {
true: <IconCurrencyDollar />,
false: undefined,
},
},
value: { control: { type: 'number' } },
},
args: {
value: 10,
icon: true,
placeholder: 'Number',
},
};
export default meta;
type Story = StoryObj<typeof NumberEditableField>;
export const Default: Story = {};

View File

@ -0,0 +1,5 @@
import { createContext } from 'react';
import { FilterDefinition } from '../types/FilterDefinition';
export const AvailableFiltersContext = createContext<FilterDefinition[]>([]);

View File

@ -12,7 +12,7 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../types/ViewField';
} from '../../editable-field/types/ViewField';
const StyledColumnMenu = styled(DropdownMenu)`
font-weight: ${({ theme }) => theme.font.weight.regular};

View File

@ -13,6 +13,10 @@ import {
useUpdateViewFieldMutation,
} from '~/generated/graphql';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../../editable-field/types/ViewField';
import { toViewFieldInput } from '../hooks/useLoadView';
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
import {
@ -21,10 +25,6 @@ import {
viewFieldsState,
visibleViewFieldsState,
} from '../states/viewFieldsState';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../types/ViewField';
import { ColumnHead } from './ColumnHead';
import { EntityTableColumnMenu } from './EntityTableColumnMenu';

View File

@ -1,10 +1,10 @@
import { defaultOrderBy } from '@/people/queries';
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData';
import { useLoadView } from '../hooks/useLoadView';

View File

@ -1,17 +1,17 @@
import { isViewFieldDate } from '@/ui/editable-field/types/guards/isViewFieldDate';
import { isViewFieldDoubleText } from '@/ui/editable-field/types/guards/isViewFieldDoubleText';
import { isViewFieldDoubleTextChip } from '@/ui/editable-field/types/guards/isViewFieldDoubleTextChip';
import { isViewFieldNumber } from '@/ui/editable-field/types/guards/isViewFieldNumber';
import { isViewFieldPhone } from '@/ui/editable-field/types/guards/isViewFieldPhone';
import { isViewFieldRelation } from '@/ui/editable-field/types/guards/isViewFieldRelation';
import { isViewFieldText } from '@/ui/editable-field/types/guards/isViewFieldText';
import { isViewFieldURL } from '@/ui/editable-field/types/guards/isViewFieldURL';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { isViewFieldChip } from '../../types/guards/isViewFieldChip';
import { isViewFieldDate } from '../../types/guards/isViewFieldDate';
import { isViewFieldDoubleText } from '../../types/guards/isViewFieldDoubleText';
import { isViewFieldDoubleTextChip } from '../../types/guards/isViewFieldDoubleTextChip';
import { isViewFieldNumber } from '../../types/guards/isViewFieldNumber';
import { isViewFieldPhone } from '../../types/guards/isViewFieldPhone';
import { isViewFieldRelation } from '../../types/guards/isViewFieldRelation';
import { isViewFieldText } from '../../types/guards/isViewFieldText';
import { isViewFieldURL } from '../../types/guards/isViewFieldURL';
import { isViewFieldChip } from '../../../editable-field/types/guards/isViewFieldChip';
import { GenericEditableChipCell } from '../type/components/GenericEditableChipCell';
import { GenericEditableDateCell } from '../type/components/GenericEditableDateCell';
import { GenericEditableDoubleTextCell } from '../type/components/GenericEditableDoubleTextCell';

View File

@ -1,8 +1,8 @@
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import {
ViewFieldChipMetadata,
ViewFieldDefinition,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { GenericEditableChipCellDisplayMode } from './GenericEditableChipCellDisplayMode';
import { GenericEditableChipCellEditMode } from './GenericEditableChipCellEditMode';

View File

@ -1,13 +1,13 @@
import { useRecoilValue } from 'recoil';
import { CompanyChip } from '@/companies/components/CompanyChip';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldChipMetadata,
ViewFieldDefinition,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { getLogoUrlFromDomainName } from '~/utils';
type OwnProps = {

View File

@ -1,12 +1,12 @@
import { useRecoilState } from 'recoil';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldChipMetadata,
ViewFieldDefinition,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { TextCellEdit } from './TextCellEdit';

View File

@ -1,13 +1,13 @@
import { useRecoilValue } from 'recoil';
import {
ViewFieldDateMetadata,
ViewFieldDefinition,
} from '@/ui/editable-field/types/ViewField';
import { DateInputDisplay } from '@/ui/input/date/components/DateInputDisplay';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDateMetadata,
ViewFieldDefinition,
} from '@/ui/table/types/ViewField';
import { GenericEditableDateCellEditMode } from './GenericEditableDateCellEditMode';

View File

@ -1,13 +1,13 @@
import { DateTime } from 'luxon';
import { useRecoilState } from 'recoil';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDateMetadata,
ViewFieldDefinition,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { DateCellEdit } from './DateCellEdit';

View File

@ -1,13 +1,13 @@
import { useRecoilValue } from 'recoil';
import {
ViewFieldDefinition,
ViewFieldDoubleTextMetadata,
} from '@/ui/editable-field/types/ViewField';
import { TextInputDisplay } from '@/ui/input/text/components/TextInputDisplay';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldDoubleTextMetadata,
} from '@/ui/table/types/ViewField';
import { GenericEditableDoubleTextCellEditMode } from './GenericEditableDoubleTextCellEditMode';

View File

@ -1,12 +1,12 @@
import { useRecoilState } from 'recoil';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldDoubleTextMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { DoubleTextCellEdit } from './DoubleTextCellEdit';

View File

@ -1,9 +1,9 @@
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope';
import {
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope';
import { GenericEditableDoubleTextChipCellDisplayMode } from './GenericEditableDoubleTextChipCellDisplayMode';
import { GenericEditableDoubleTextChipCellEditMode } from './GenericEditableDoubleTextChipCellEditMode';

View File

@ -2,13 +2,13 @@ import { useRecoilState } from 'recoil';
import { CompanyChip } from '@/companies/components/CompanyChip';
import { PersonChip } from '@/people/components/PersonChip';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
type OwnProps = {
viewField: ViewFieldDefinition<ViewFieldDoubleTextChipMetadata>;

View File

@ -1,12 +1,12 @@
import { useRecoilState } from 'recoil';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldDoubleTextChipMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { DoubleTextCellEdit } from './DoubleTextCellEdit';

View File

@ -1,12 +1,12 @@
import { useRecoilValue } from 'recoil';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldNumberMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { GenericEditableNumberCellEditMode } from './GenericEditableNumberCellEditMode';

View File

@ -1,12 +1,12 @@
import { useRecoilState } from 'recoil';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldNumberMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { TextCellEdit } from './TextCellEdit';

View File

@ -1,13 +1,13 @@
import { useRecoilValue } from 'recoil';
import {
ViewFieldDefinition,
ViewFieldPhoneMetadata,
} from '@/ui/editable-field/types/ViewField';
import { PhoneInputDisplay } from '@/ui/input/phone/components/PhoneInputDisplay';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldPhoneMetadata,
} from '@/ui/table/types/ViewField';
import { GenericEditablePhoneCellEditMode } from './GenericEditablePhoneCellEditMode';

View File

@ -1,12 +1,12 @@
import { useRecoilState } from 'recoil';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldPhoneMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { TextCellEdit } from './TextCellEdit';

View File

@ -1,9 +1,9 @@
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import {
ViewFieldDefinition,
ViewFieldRelationMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { GenericEditableRelationCellDisplayMode } from './GenericEditableRelationCellDisplayMode';
import { GenericEditableRelationCellEditMode } from './GenericEditableRelationCellEditMode';

View File

@ -1,13 +1,13 @@
import { useRecoilValue } from 'recoil';
import { CompanyChip } from '@/companies/components/CompanyChip';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldRelationMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { UserChip } from '@/users/components/UserChip';
import { getLogoUrlFromDomainName } from '~/utils';

View File

@ -1,16 +1,16 @@
import { useRecoilState } from 'recoil';
import { CompanyPickerCell } from '@/companies/components/CompanyPickerCell';
import {
ViewFieldDefinition,
ViewFieldRelationMetadata,
} from '@/ui/editable-field/types/ViewField';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
import { useEditableCell } from '@/ui/table/editable-cell/hooks/useEditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldRelationMetadata,
} from '@/ui/table/types/ViewField';
import { UserPicker } from '@/users/components/UserPicker';
type OwnProps = {

View File

@ -1,13 +1,13 @@
import { useRecoilValue } from 'recoil';
import {
ViewFieldDefinition,
ViewFieldTextMetadata,
} from '@/ui/editable-field/types/ViewField';
import { TextInputDisplay } from '@/ui/input/text/components/TextInputDisplay';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldTextMetadata,
} from '@/ui/table/types/ViewField';
import { GenericEditableTextCellEditMode } from './GenericEditableTextCellEditMode';

View File

@ -1,12 +1,12 @@
import { useRecoilState } from 'recoil';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldTextMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { TextCellEdit } from './TextCellEdit';

View File

@ -1,13 +1,13 @@
import { useRecoilValue } from 'recoil';
import {
ViewFieldDefinition,
ViewFieldURLMetadata,
} from '@/ui/editable-field/types/ViewField';
import { InplaceInputURLDisplayMode } from '@/ui/input/url/components/URLTextInputDisplay';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldURLMetadata,
} from '@/ui/table/types/ViewField';
import { sanitizeURL } from '~/utils';
import { GenericEditableURLCellEditMode } from './GenericEditableURLCellEditMode';

View File

@ -1,12 +1,12 @@
import { useRecoilState } from 'recoil';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import {
ViewFieldDefinition,
ViewFieldURLMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { TextCellEdit } from './TextCellEdit';

View File

@ -8,13 +8,13 @@ import {
useGetViewFieldsQuery,
} from '~/generated/graphql';
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
import { viewFieldsState } from '../states/viewFieldsState';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldTextMetadata,
} from '../types/ViewField';
} from '../../editable-field/types/ViewField';
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
import { viewFieldsState } from '../states/viewFieldsState';
const DEFAULT_VIEW_FIELD_METADATA: ViewFieldTextMetadata = {
type: 'text',

View File

@ -1,25 +1,25 @@
import { useContext } from 'react';
import { isViewFieldChip } from '@/ui/editable-field/types/guards/isViewFieldChip';
import { isViewFieldDate } from '@/ui/editable-field/types/guards/isViewFieldDate';
import { isViewFieldDateValue } from '@/ui/editable-field/types/guards/isViewFieldDateValue';
import { isViewFieldDoubleText } from '@/ui/editable-field/types/guards/isViewFieldDoubleText';
import { isViewFieldDoubleTextChip } from '@/ui/editable-field/types/guards/isViewFieldDoubleTextChip';
import { isViewFieldDoubleTextChipValue } from '@/ui/editable-field/types/guards/isViewFieldDoubleTextChipValue';
import { isViewFieldDoubleTextValue } from '@/ui/editable-field/types/guards/isViewFieldDoubleTextValue';
import { isViewFieldNumber } from '@/ui/editable-field/types/guards/isViewFieldNumber';
import { isViewFieldNumberValue } from '@/ui/editable-field/types/guards/isViewFieldNumberValue';
import { isViewFieldPhone } from '@/ui/editable-field/types/guards/isViewFieldPhone';
import { isViewFieldPhoneValue } from '@/ui/editable-field/types/guards/isViewFieldPhoneValue';
import { isViewFieldRelation } from '@/ui/editable-field/types/guards/isViewFieldRelation';
import { isViewFieldRelationValue } from '@/ui/editable-field/types/guards/isViewFieldRelationValue';
import { isViewFieldText } from '@/ui/editable-field/types/guards/isViewFieldText';
import { isViewFieldTextValue } from '@/ui/editable-field/types/guards/isViewFieldTextValue';
import { isViewFieldURL } from '@/ui/editable-field/types/guards/isViewFieldURL';
import { isViewFieldURLValue } from '@/ui/editable-field/types/guards/isViewFieldURLValue';
import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext';
import { isViewFieldChip } from '@/ui/table/types/guards/isViewFieldChip';
import { isViewFieldRelation } from '@/ui/table/types/guards/isViewFieldRelation';
import { isViewFieldText } from '@/ui/table/types/guards/isViewFieldText';
import { isViewFieldChipValue } from '../types/guards/isViewFieldChipValue';
import { isViewFieldDate } from '../types/guards/isViewFieldDate';
import { isViewFieldDateValue } from '../types/guards/isViewFieldDateValue';
import { isViewFieldDoubleText } from '../types/guards/isViewFieldDoubleText';
import { isViewFieldDoubleTextChip } from '../types/guards/isViewFieldDoubleTextChip';
import { isViewFieldDoubleTextChipValue } from '../types/guards/isViewFieldDoubleTextChipValue';
import { isViewFieldDoubleTextValue } from '../types/guards/isViewFieldDoubleTextValue';
import { isViewFieldNumber } from '../types/guards/isViewFieldNumber';
import { isViewFieldNumberValue } from '../types/guards/isViewFieldNumberValue';
import { isViewFieldPhone } from '../types/guards/isViewFieldPhone';
import { isViewFieldPhoneValue } from '../types/guards/isViewFieldPhoneValue';
import { isViewFieldRelationValue } from '../types/guards/isViewFieldRelationValue';
import { isViewFieldTextValue } from '../types/guards/isViewFieldTextValue';
import { isViewFieldURL } from '../types/guards/isViewFieldURL';
import { isViewFieldURLValue } from '../types/guards/isViewFieldURLValue';
import { isViewFieldChipValue } from '../../editable-field/types/guards/isViewFieldChipValue';
import {
ViewFieldChipMetadata,
ViewFieldChipValue,
@ -41,7 +41,7 @@ import {
ViewFieldTextValue,
ViewFieldURLMetadata,
ViewFieldURLValue,
} from '../types/ViewField';
} from '../../editable-field/types/ViewField';
export function useUpdateEntityField() {
const useUpdateEntityMutation = useContext(EntityUpdateMutationHookContext);

View File

@ -1,6 +1,9 @@
import { createContext } from 'react';
import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../../editable-field/types/ViewField';
export const ViewFieldContext =
createContext<ViewFieldDefinition<ViewFieldMetadata> | null>(null);

View File

@ -6,7 +6,7 @@ import { peopleViewFields } from '@/people/constants/peopleViewFields';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../types/ViewField';
} from '../../editable-field/types/ViewField';
export const viewFieldsState = atom<{
objectName: 'company' | 'person' | '';

View File

@ -8,6 +8,10 @@ import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { IconChevronLeft, IconMinus, IconPlus, IconTag } from '@/ui/icon';
@ -15,10 +19,6 @@ import {
hiddenViewFieldsState,
visibleViewFieldsState,
} from '@/ui/table/states/viewFieldsState';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/table/types/ViewField';
import { useUpdateViewFieldMutation } from '~/generated/graphql';
import { GET_VIEW_FIELDS } from '../queries/select';

View File

@ -10,7 +10,7 @@ import { DropdownMenuSubheader } from '@/ui/dropdown/components/DropdownMenuSubh
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/table/types/ViewField';
} from '@/ui/editable-field/types/ViewField';
type OptionsDropdownSectionProps = {
renderActions: (

View File

@ -14,6 +14,7 @@ import { EntityBoard } from '@/ui/board/components/EntityBoard';
import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar';
import { BoardOptionsContext } from '@/ui/board/states/BoardOptionsContext';
import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers';
import { AvailableFiltersContext } from '@/ui/filter-n-sort/states/AvailableFiltersContext';
import { IconTargetArrow } from '@/ui/icon/index';
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
@ -83,19 +84,20 @@ export function Opportunities() {
>
<BoardOptionsContext.Provider value={opportunitiesBoardOptions}>
<RecoilScope SpecificContext={CompanyBoardContext}>
<HooksCompanyBoard
availableFilters={opportunitiesBoardOptions.filters}
orderBy={orderBy}
/>
<EntityBoard
boardOptions={opportunitiesBoardOptions}
updateSorts={updateSorts}
onEditColumnColor={handleEditColumnColor}
onEditColumnTitle={handleEditColumnTitle}
/>
<EntityBoardActionBar>
<BoardActionBarButtonDeleteBoardCard onDelete={handleDelete} />
</EntityBoardActionBar>
<AvailableFiltersContext.Provider
value={opportunitiesBoardOptions.filters}
>
<HooksCompanyBoard orderBy={orderBy} />
<EntityBoard
boardOptions={opportunitiesBoardOptions}
updateSorts={updateSorts}
onEditColumnColor={handleEditColumnColor}
onEditColumnTitle={handleEditColumnTitle}
/>
<EntityBoardActionBar>
<BoardActionBarButtonDeleteBoardCard onDelete={handleDelete} />
</EntityBoardActionBar>
</AvailableFiltersContext.Provider>
</RecoilScope>
</BoardOptionsContext.Provider>
</WithTopBarContainer>

View File

@ -129,11 +129,13 @@ export function AuthAutoRouter() {
}
}
eventTracker('pageview', {
location: {
pathname: location.pathname,
},
});
setTimeout(() => {
eventTracker('pageview', {
location: {
pathname: location.pathname,
},
});
}, 500);
}, [
onboardingStatus,
navigate,

Some files were not shown because too many files have changed in this diff Show More