Feat/editable fields update (#743)

* Removed console log

* Used current scope as default parent scope for fields

* Finished editable fields on people show page

* Added stories

* Console log

* Lint
This commit is contained in:
Lucas Bordeau
2023-07-19 02:43:16 +02:00
committed by GitHub
parent 5ee8eaa985
commit 7ecb098c55
30 changed files with 521 additions and 49 deletions

View File

@ -1,7 +1,7 @@
import { formatToHumanReadableDate } from '~/utils';
type OwnProps = {
value: Date;
value: Date | null;
};
export function InplaceInputDateDisplayMode({ value }: OwnProps) {

View File

@ -16,7 +16,6 @@ export const EditableFieldNormalModeOuterContainer = styled.div<
padding: ${({ theme }) => theme.spacing(1)};
${(props) => {
console.log(props.isDisplayModeContentEmpty);
if (props.isDisplayModeContentEmpty) {
return css`
min-width: 50px;

View File

@ -1,8 +1,13 @@
import { useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { currentHotkeyScopeState } from '@/ui/hotkey/states/internal/currentHotkeyScopeState';
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
import { isSameHotkeyScope } from '@/ui/hotkey/utils/isSameHotkeyScope';
import { useContextScopeId } from '@/ui/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { getSnapshotScopedState } from '@/ui/recoil-scope/utils/getSnapshotScopedState';
import { getSnapshotState } from '@/ui/recoil-scope/utils/getSnapshotState';
import { customEditHotkeyScopeForFieldScopedState } from '../states/customEditHotkeyScopeForFieldScopedState';
import { FieldContext } from '../states/FieldContext';
@ -21,8 +26,7 @@ export function useBindFieldHotkeyScope({
FieldContext,
);
const [parentHotkeyScopeForField, setParentHotkeyScopeForField] =
useRecoilScopedState(parentHotkeyScopeForFieldScopedState, FieldContext);
const fieldContextScopeId = useContextScopeId(FieldContext);
useEffect(() => {
if (
@ -37,16 +41,35 @@ export function useBindFieldHotkeyScope({
setCustomEditHotkeyScopeForField,
]);
const setParentHotkeyScopeForField = useRecoilCallback(
({ snapshot, set }) =>
(parentHotkeyScopeToSet: HotkeyScope | null | undefined) => {
const currentHotkeyScope = getSnapshotState({
snapshot,
state: currentHotkeyScopeState,
});
const parentHotkeyScopeForField = getSnapshotScopedState({
snapshot,
state: parentHotkeyScopeForFieldScopedState,
contextScopeId: fieldContextScopeId,
});
if (!parentHotkeyScopeToSet) {
set(
parentHotkeyScopeForFieldScopedState(fieldContextScopeId),
currentHotkeyScope,
);
} else if (
!isSameHotkeyScope(parentHotkeyScopeToSet, parentHotkeyScopeForField)
) {
setParentHotkeyScopeForField(parentHotkeyScopeToSet);
}
},
[fieldContextScopeId],
);
useEffect(() => {
if (
parentHotkeyScope &&
!isSameHotkeyScope(parentHotkeyScope, parentHotkeyScopeForField)
) {
setParentHotkeyScopeForField(parentHotkeyScope);
}
}, [
parentHotkeyScope,
parentHotkeyScopeForField,
setParentHotkeyScopeForField,
]);
setParentHotkeyScopeForField(parentHotkeyScope);
}, [parentHotkeyScope, setParentHotkeyScopeForField]);
}

View File

@ -7,7 +7,6 @@ import { isFieldInEditModeScopedState } from '../states/isFieldInEditModeScopedS
import { parentHotkeyScopeForFieldScopedState } from '../states/parentHotkeyScopeForFieldScopedState';
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
// TODO: use atoms for hotkey scopes
export function useEditableField() {
const [isFieldInEditMode, setIsFieldInEditMode] = useRecoilScopedState(
isFieldInEditModeScopedState,

View File

@ -0,0 +1,65 @@
import { useEffect, useState } from 'react';
import { InplaceInputDateDisplayMode } from '@/ui/display/component/InplaceInputDateDisplayMode';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { parseDate } from '~/utils/date-utils';
import { EditableFieldEditModeDate } from './EditableFieldEditModeDate';
type OwnProps = {
icon?: React.ReactNode;
value: string | null | undefined;
onSubmit?: (newValue: string) => void;
};
export function DateEditableField({ icon, value, 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;
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
iconLabel={icon}
editModeContent={
<EditableFieldEditModeDate
value={internalValue ?? ''}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
}
displayModeContent={
<InplaceInputDateDisplayMode value={internalDateValue} />
}
isDisplayModeContentEmpty={!(internalValue !== '')}
/>
</RecoilScope>
);
}

View File

@ -2,7 +2,7 @@ import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
import { InplaceInputDate } from '@/ui/inplace-input/components/InplaceInputDate';
import { parseDate } from '~/utils/date-utils';
import { useEditableField } from '../hooks/useEditableField';
import { useEditableField } from '../../hooks/useEditableField';
type OwnProps = {
value: string;

View File

@ -0,0 +1,66 @@
import { useEffect, useState } from 'react';
import { InplaceInputPhoneDisplayMode } from '@/ui/display/component/InplaceInputPhoneDisplayMode';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
type OwnProps = {
icon?: React.ReactNode;
placeholder?: string;
value: string | null | undefined;
onSubmit?: (newValue: string) => void;
};
export function PhoneEditableField({
icon,
placeholder,
value,
onSubmit,
}: OwnProps) {
const [internalValue, setInternalValue] = useState(value);
useEffect(() => {
setInternalValue(value);
}, [value]);
async function handleChange(newValue: string) {
setInternalValue(newValue);
}
async function handleSubmit() {
if (!internalValue) return;
onSubmit?.(internalValue);
}
async function handleCancel() {
setInternalValue(value);
}
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
iconLabel={icon}
editModeContent={
<InplaceInputText
placeholder={placeholder ?? ''}
autoFocus
value={internalValue ?? ''}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
}
displayModeContent={
<InplaceInputPhoneDisplayMode value={internalValue ?? ''} />
}
isDisplayModeContentEmpty={!(internalValue !== '')}
useEditButton
/>
</RecoilScope>
);
}

View File

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconCalendar } from '@tabler/icons-react';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { DateEditableField } from '../DateEditableField';
const meta: Meta<typeof DateEditableField> = {
title: 'UI/EditableField/DateEditableField',
component: DateEditableField,
};
export default meta;
type Story = StoryObj<typeof DateEditableField>;
export const Default: Story = {
render: getRenderWrapperForComponent(
<DateEditableField
value={new Date().toISOString()}
icon={<IconCalendar />}
/>,
),
};

View File

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconCurrencyDollar } from '@tabler/icons-react';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { NumberEditableField } from '../NumberEditableField';
const meta: Meta<typeof NumberEditableField> = {
title: 'UI/EditableField/NumberEditableField',
component: NumberEditableField,
};
export default meta;
type Story = StoryObj<typeof NumberEditableField>;
export const Default: Story = {
render: getRenderWrapperForComponent(
<NumberEditableField
value={10}
icon={<IconCurrencyDollar />}
placeholder="Number"
/>,
),
};

View File

@ -0,0 +1,27 @@
import { BrowserRouter } from 'react-router-dom';
import type { Meta, StoryObj } from '@storybook/react';
import { IconPhone } from '@tabler/icons-react';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { PhoneEditableField } from '../PhoneEditableField';
const meta: Meta<typeof PhoneEditableField> = {
title: 'UI/EditableField/PhoneEditableField',
component: PhoneEditableField,
};
export default meta;
type Story = StoryObj<typeof PhoneEditableField>;
export const Default: Story = {
render: getRenderWrapperForComponent(
<BrowserRouter>
<PhoneEditableField
value={'+33714446494'}
icon={<IconPhone />}
placeholder="Phone"
/>
</BrowserRouter>,
),
};

View File

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';
import { IconUser } from '@tabler/icons-react';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { TextEditableField } from '../TextEditableField';
const meta: Meta<typeof TextEditableField> = {
title: 'UI/EditableField/TextEditableField',
component: TextEditableField,
};
export default meta;
type Story = StoryObj<typeof TextEditableField>;
export const Default: Story = {
render: getRenderWrapperForComponent(
<TextEditableField
value={'John Doe'}
icon={<IconUser />}
placeholder="Name"
/>,
),
};

View File

@ -1,5 +1,5 @@
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { useRecoilCallback } from 'recoil';
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
@ -13,8 +13,6 @@ export function usePreviousHotkeyScope() {
const setHotkeyScope = useSetHotkeyScope();
const currentHotkeyScope = useRecoilValue(currentHotkeyScopeState);
function goBackToPreviousHotkeyScope() {
if (previousHotkeyScope) {
setHotkeyScope(
@ -24,13 +22,19 @@ export function usePreviousHotkeyScope() {
}
}
function setHotkeyScopeAndMemorizePreviousScope(
scope: string,
customScopes?: CustomHotkeyScopes,
) {
setPreviousHotkeyScope(currentHotkeyScope);
setHotkeyScope(scope, customScopes);
}
const setHotkeyScopeAndMemorizePreviousScope = useRecoilCallback(
({ snapshot }) =>
(scope: string, customScopes?: CustomHotkeyScopes) => {
const currentHotkeyScope = snapshot
.getLoadable(currentHotkeyScopeState)
.valueOrThrow();
setHotkeyScope(scope, customScopes);
setPreviousHotkeyScope(currentHotkeyScope);
},
[setPreviousHotkeyScope],
);
return {
setHotkeyScopeAndMemorizePreviousScope,

View File

@ -19,12 +19,12 @@ function isCustomScopesEqual(
export function useSetHotkeyScope() {
return useRecoilCallback(
({ snapshot, set }) =>
async (HotkeyScopeToSet: string, customScopes?: CustomHotkeyScopes) => {
async (hotkeyScopeToSet: string, customScopes?: CustomHotkeyScopes) => {
const currentHotkeyScope = await snapshot.getPromise(
currentHotkeyScopeState,
);
if (currentHotkeyScope.scope === HotkeyScopeToSet) {
if (currentHotkeyScope.scope === hotkeyScopeToSet) {
if (!isDefined(customScopes)) {
if (
isCustomScopesEqual(
@ -47,7 +47,7 @@ export function useSetHotkeyScope() {
}
set(currentHotkeyScopeState, {
scope: HotkeyScopeToSet,
scope: hotkeyScopeToSet,
customScopes: {
commandMenu: customScopes?.commandMenu ?? true,
goto: customScopes?.goto ?? false,

View File

@ -48,7 +48,7 @@ export const DatePickerContainer = ({ children }: DatePickerContainerProps) => {
};
type OwnProps = {
value: Date;
value: Date | null | undefined;
onChange: (newDate: Date) => void;
};
@ -56,7 +56,7 @@ export function InplaceInputDate({ onChange, value }: OwnProps) {
return (
<InplaceInputContainer>
<DatePicker
date={value}
date={value ?? new Date()}
onChangeHandler={onChange}
customInput={<DateDisplay />}
customCalendarContainer={DatePickerContainer}

View File

@ -0,0 +1,12 @@
import { Context, useContext } from 'react';
export function useContextScopeId(SpecificContext: Context<string | null>) {
const recoilScopeId = useContext(SpecificContext);
if (!recoilScopeId)
throw new Error(
`Using useContextScopedId outside of the specified context : ${SpecificContext.displayName}, verify that you are using a RecoilScope with the specific context you want to use.`,
);
return recoilScopeId;
}

View File

@ -0,0 +1,13 @@
import { RecoilState, Snapshot } from 'recoil';
export function getSnapshotScopedState<T>({
snapshot,
state,
contextScopeId,
}: {
snapshot: Snapshot;
state: (scopeId: string) => RecoilState<T>;
contextScopeId: string;
}) {
return snapshot.getLoadable(state(contextScopeId)).valueOrThrow();
}

View File

@ -0,0 +1,11 @@
import { RecoilState, Snapshot } from 'recoil';
export function getSnapshotState<T>({
snapshot,
state,
}: {
snapshot: Snapshot;
state: RecoilState<T>;
}) {
return snapshot.getLoadable(state).valueOrThrow();
}

View File

@ -25,6 +25,7 @@ export function EditableCellDateEditMode({
function handleDateChange(newDate: Date) {
onChange(newDate);
closeEditableCell();
}