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

@ -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,26 +0,0 @@
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { ProbabilityFieldEditMode } from './ProbabilityFieldEditMode';
type OwnProps = {
icon?: React.ReactNode;
value: number | null | undefined;
onSubmit?: (newValue: number) => void;
};
export function ProbabilityEditableField({ icon, value, onSubmit }: OwnProps) {
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
iconLabel={icon}
displayModeContentOnly
disableHoverEffect
displayModeContent={
<ProbabilityFieldEditMode value={value ?? 0} onChange={onSubmit} />
}
/>
</RecoilScope>
);
}

View File

@ -1,79 +0,0 @@
import { useEffect, useState } from 'react';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { 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
}
}
`;