Reorganize frontend and install Craco to alias modules (#190)
This commit is contained in:
@ -0,0 +1,12 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const CellBaseContainer = styled.div`
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
`;
|
||||
@ -0,0 +1,29 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { overlayBackground } from '../../layout/styles/themes';
|
||||
|
||||
type OwnProps = {
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
};
|
||||
|
||||
export const CellEditModeContainer = styled.div<OwnProps>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
padding-left: ${(props) => props.theme.spacing(2)};
|
||||
padding-right: ${(props) => props.theme.spacing(2)};
|
||||
margin-left: -2px;
|
||||
position: absolute;
|
||||
left: ${(props) =>
|
||||
props.editModeHorizontalAlign === 'right' ? 'auto' : '0'};
|
||||
right: ${(props) =>
|
||||
props.editModeHorizontalAlign === 'right' ? '0' : 'auto'};
|
||||
top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')};
|
||||
|
||||
border: 1px solid ${(props) => props.theme.primaryBorder};
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
${overlayBackground}
|
||||
`;
|
||||
@ -0,0 +1,12 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const CellNormalModeContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
padding-left: ${(props) => props.theme.spacing(2)};
|
||||
padding-right: ${(props) => props.theme.spacing(2)};
|
||||
`;
|
||||
@ -0,0 +1,58 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { isSomeInputInEditModeState } from '../../tables/states/isSomeInputInEditModeState';
|
||||
|
||||
import { CellBaseContainer } from './CellBaseContainer';
|
||||
import { CellNormalModeContainer } from './CellNormalModeContainer';
|
||||
import { EditableCellEditMode } from './EditableCellEditMode';
|
||||
|
||||
type OwnProps = {
|
||||
editModeContent: ReactElement;
|
||||
nonEditModeContent: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
isEditMode?: boolean;
|
||||
isCreateMode?: boolean;
|
||||
onOutsideClick?: () => void;
|
||||
onInsideClick?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCell({
|
||||
editModeContent,
|
||||
nonEditModeContent,
|
||||
editModeHorizontalAlign = 'left',
|
||||
editModeVerticalPosition = 'over',
|
||||
isEditMode = false,
|
||||
onOutsideClick,
|
||||
onInsideClick,
|
||||
}: OwnProps) {
|
||||
const [isSomeInputInEditMode, setIsSomeInputInEditMode] = useRecoilState(
|
||||
isSomeInputInEditModeState,
|
||||
);
|
||||
|
||||
function handleOnClick() {
|
||||
if (!isSomeInputInEditMode) {
|
||||
onInsideClick?.();
|
||||
setIsSomeInputInEditMode(true);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<CellBaseContainer onClick={handleOnClick}>
|
||||
{isEditMode ? (
|
||||
<EditableCellEditMode
|
||||
editModeContent={editModeContent}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={onOutsideClick}
|
||||
/>
|
||||
) : (
|
||||
<CellNormalModeContainer>
|
||||
<>{nonEditModeContent}</>
|
||||
</CellNormalModeContainer>
|
||||
)}
|
||||
</CellBaseContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
import { ReactElement, useMemo, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { debounce } from '@/utils/debounce';
|
||||
|
||||
import { useListenClickOutsideArrayOfRef } from '../../hooks/useListenClickOutsideArrayOfRef';
|
||||
import { isSomeInputInEditModeState } from '../../tables/states/isSomeInputInEditModeState';
|
||||
|
||||
import { CellEditModeContainer } from './CellEditModeContainer';
|
||||
|
||||
type OwnProps = {
|
||||
editModeContent: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
isEditMode?: boolean;
|
||||
onOutsideClick?: () => void;
|
||||
onInsideClick?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCellEditMode({
|
||||
editModeHorizontalAlign,
|
||||
editModeVerticalPosition,
|
||||
editModeContent,
|
||||
isEditMode,
|
||||
onOutsideClick,
|
||||
}: OwnProps) {
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
const [, setIsSomeInputInEditMode] = useRecoilState(
|
||||
isSomeInputInEditModeState,
|
||||
);
|
||||
|
||||
const debouncedSetIsSomeInputInEditMode = useMemo(() => {
|
||||
return debounce(setIsSomeInputInEditMode, 20);
|
||||
}, [setIsSomeInputInEditMode]);
|
||||
|
||||
useListenClickOutsideArrayOfRef([wrapperRef], () => {
|
||||
if (isEditMode) {
|
||||
debouncedSetIsSomeInputInEditMode(false);
|
||||
onOutsideClick?.();
|
||||
}
|
||||
});
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
if (isEditMode) {
|
||||
onOutsideClick?.();
|
||||
|
||||
debouncedSetIsSomeInputInEditMode(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[isEditMode, onOutsideClick, debouncedSetIsSomeInputInEditMode],
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
if (isEditMode) {
|
||||
onOutsideClick?.();
|
||||
|
||||
debouncedSetIsSomeInputInEditMode(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[isEditMode, onOutsideClick, debouncedSetIsSomeInputInEditMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<CellEditModeContainer
|
||||
data-testid="editable-cell-edit-mode-container"
|
||||
ref={wrapperRef}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
>
|
||||
{editModeContent}
|
||||
</CellEditModeContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
import { ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { isSomeInputInEditModeState } from '../../tables/states/isSomeInputInEditModeState';
|
||||
|
||||
import { CellBaseContainer } from './CellBaseContainer';
|
||||
import { EditableCellMenuEditMode } from './EditableCellMenuEditMode';
|
||||
|
||||
const EditableCellMenuNormalModeContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: calc(100% - ${(props) => props.theme.spacing(5)});
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
editModeContent: ReactElement;
|
||||
nonEditModeContent: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
isEditMode?: boolean;
|
||||
isCreateMode?: boolean;
|
||||
onOutsideClick?: () => void;
|
||||
onInsideClick?: () => void;
|
||||
};
|
||||
|
||||
// TODO: refactor
|
||||
export function EditableCellMenu({
|
||||
editModeContent,
|
||||
nonEditModeContent,
|
||||
editModeHorizontalAlign = 'left',
|
||||
editModeVerticalPosition = 'over',
|
||||
isEditMode = false,
|
||||
onOutsideClick,
|
||||
onInsideClick,
|
||||
}: OwnProps) {
|
||||
const [isSomeInputInEditMode, setIsSomeInputInEditMode] = useRecoilState(
|
||||
isSomeInputInEditModeState,
|
||||
);
|
||||
|
||||
function handleOnClick() {
|
||||
if (!isSomeInputInEditMode) {
|
||||
onInsideClick?.();
|
||||
setIsSomeInputInEditMode(true);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<CellBaseContainer onClick={handleOnClick}>
|
||||
<EditableCellMenuNormalModeContainer>
|
||||
{nonEditModeContent}
|
||||
</EditableCellMenuNormalModeContainer>
|
||||
{isEditMode && (
|
||||
<EditableCellMenuEditMode
|
||||
editModeContent={editModeContent}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={onOutsideClick}
|
||||
onInsideClick={onInsideClick}
|
||||
/>
|
||||
)}
|
||||
</CellBaseContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
import { ReactElement, useMemo, useRef } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { debounce } from '@/utils/debounce';
|
||||
|
||||
import { useListenClickOutsideArrayOfRef } from '../../hooks/useListenClickOutsideArrayOfRef';
|
||||
import { isSomeInputInEditModeState } from '../../tables/states/isSomeInputInEditModeState';
|
||||
|
||||
import { EditableCellMenuEditModeContainer } from './EditableCellMenuEditModeContainer';
|
||||
|
||||
type OwnProps = {
|
||||
editModeContent: ReactElement;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
isEditMode?: boolean;
|
||||
onOutsideClick?: () => void;
|
||||
onInsideClick?: () => void;
|
||||
};
|
||||
|
||||
export function EditableCellMenuEditMode({
|
||||
editModeHorizontalAlign,
|
||||
editModeVerticalPosition,
|
||||
editModeContent,
|
||||
isEditMode,
|
||||
onOutsideClick,
|
||||
}: OwnProps) {
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
const [, setIsSomeInputInEditMode] = useRecoilState(
|
||||
isSomeInputInEditModeState,
|
||||
);
|
||||
|
||||
const debouncedSetIsSomeInputInEditMode = useMemo(() => {
|
||||
return debounce(setIsSomeInputInEditMode, 20);
|
||||
}, [setIsSomeInputInEditMode]);
|
||||
|
||||
useListenClickOutsideArrayOfRef([wrapperRef], () => {
|
||||
if (isEditMode) {
|
||||
debouncedSetIsSomeInputInEditMode(false);
|
||||
onOutsideClick?.();
|
||||
}
|
||||
});
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
() => {
|
||||
if (isEditMode) {
|
||||
onOutsideClick?.();
|
||||
|
||||
debouncedSetIsSomeInputInEditMode(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[isEditMode, onOutsideClick, debouncedSetIsSomeInputInEditMode],
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
if (isEditMode) {
|
||||
onOutsideClick?.();
|
||||
|
||||
debouncedSetIsSomeInputInEditMode(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[isEditMode, onOutsideClick, debouncedSetIsSomeInputInEditMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<EditableCellMenuEditModeContainer
|
||||
ref={wrapperRef}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeVerticalPosition={editModeVerticalPosition}
|
||||
>
|
||||
{editModeContent}
|
||||
</EditableCellMenuEditModeContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { overlayBackground } from '../../layout/styles/themes';
|
||||
|
||||
type OwnProps = {
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
editModeVerticalPosition?: 'over' | 'below';
|
||||
};
|
||||
|
||||
// TODO: refactor
|
||||
export const EditableCellMenuEditModeContainer = styled.div<OwnProps>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
position: absolute;
|
||||
left: ${(props) =>
|
||||
props.editModeHorizontalAlign === 'right' ? 'auto' : '-1px'};
|
||||
right: ${(props) =>
|
||||
props.editModeHorizontalAlign === 'right' ? '0' : 'auto'};
|
||||
top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')};
|
||||
|
||||
border: 1px solid ${(props) => props.theme.primaryBorder};
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
${overlayBackground}
|
||||
`;
|
||||
@ -0,0 +1,98 @@
|
||||
import { ChangeEvent, ComponentType, useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { CellCommentChip } from '@/comments/components/comments/CellCommentChip';
|
||||
|
||||
import { textInputStyle } from '../../layout/styles/themes';
|
||||
|
||||
import { EditableCell } from './EditableCell';
|
||||
|
||||
export type EditableChipProps = {
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
picture: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
ChipComponent: ComponentType<{ name: string; picture: string }>;
|
||||
commentCount?: number;
|
||||
onCommentClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
};
|
||||
|
||||
// TODO: refactor
|
||||
const StyledInplaceInput = styled.input`
|
||||
width: 100%;
|
||||
padding-left: ${(props) => props.theme.spacing(1)};
|
||||
padding-right: ${(props) => props.theme.spacing(1)};
|
||||
|
||||
${textInputStyle}
|
||||
`;
|
||||
|
||||
const StyledInplaceShow = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
&::placeholder {
|
||||
font-weight: 'bold';
|
||||
color: props.theme.text20;
|
||||
}
|
||||
`;
|
||||
|
||||
function EditableChip({
|
||||
value,
|
||||
placeholder,
|
||||
changeHandler,
|
||||
picture,
|
||||
editModeHorizontalAlign,
|
||||
ChipComponent,
|
||||
commentCount,
|
||||
onCommentClick,
|
||||
}: EditableChipProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
const showComment = commentCount ? commentCount > 0 : false;
|
||||
|
||||
function handleCommentClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onCommentClick?.(event);
|
||||
}
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
onOutsideClick={() => setIsEditMode(false)}
|
||||
onInsideClick={() => setIsEditMode(true)}
|
||||
isEditMode={isEditMode}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<StyledInplaceInput
|
||||
placeholder={placeholder || ''}
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
changeHandler(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={
|
||||
<>
|
||||
<StyledInplaceShow>
|
||||
<ChipComponent name={inputValue} picture={picture} />
|
||||
</StyledInplaceShow>
|
||||
{showComment && (
|
||||
<CellCommentChip
|
||||
count={commentCount ?? 0}
|
||||
onClick={handleCommentClick}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableChip;
|
||||
@ -0,0 +1,87 @@
|
||||
import { forwardRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { humanReadableDate } from '@/utils/utils';
|
||||
|
||||
import DatePicker from '../form/DatePicker';
|
||||
|
||||
import { EditableCell } from './EditableCell';
|
||||
|
||||
export type EditableDateProps = {
|
||||
value: Date;
|
||||
changeHandler: (date: Date) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export type StyledCalendarContainerProps = {
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
};
|
||||
|
||||
const StyledCalendarContainer = styled.div<StyledCalendarContainerProps>`
|
||||
position: absolute;
|
||||
border: 1px solid ${(props) => props.theme.primaryBorder};
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.09);
|
||||
z-index: 1;
|
||||
left: -10px;
|
||||
top: 10px;
|
||||
background: ${(props) => props.theme.secondaryBackground};
|
||||
`;
|
||||
export function EditableDate({
|
||||
value,
|
||||
changeHandler,
|
||||
editModeHorizontalAlign,
|
||||
}: EditableDateProps) {
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
type DivProps = React.HTMLProps<HTMLDivElement>;
|
||||
|
||||
const DateDisplay = forwardRef<HTMLDivElement, DivProps>(
|
||||
({ value, onClick }, ref) => (
|
||||
<div onClick={onClick} ref={ref}>
|
||||
{value && humanReadableDate(new Date(value as string))}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
interface DatePickerContainerProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DatePickerContainer = ({ children }: DatePickerContainerProps) => {
|
||||
return <StyledCalendarContainer>{children}</StyledCalendarContainer>;
|
||||
};
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={() => setIsEditMode(false)}
|
||||
onInsideClick={() => setIsEditMode(true)}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<StyledContainer>
|
||||
<DatePicker
|
||||
date={inputValue}
|
||||
onChangeHandler={(date: Date) => {
|
||||
changeHandler(date);
|
||||
setInputValue(date);
|
||||
}}
|
||||
customInput={<DateDisplay />}
|
||||
customCalendarContainer={DatePickerContainer}
|
||||
/>
|
||||
</StyledContainer>
|
||||
}
|
||||
nonEditModeContent={
|
||||
<StyledContainer>
|
||||
<div>{inputValue && humanReadableDate(inputValue)}</div>
|
||||
</StyledContainer>
|
||||
}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
import { ChangeEvent, ReactElement, useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { textInputStyle } from '../../layout/styles/themes';
|
||||
|
||||
import { EditableCell } from './EditableCell';
|
||||
|
||||
type OwnProps = {
|
||||
firstValue: string;
|
||||
secondValue: string;
|
||||
firstValuePlaceholder: string;
|
||||
secondValuePlaceholder: string;
|
||||
nonEditModeContent: ReactElement;
|
||||
onChange: (firstValue: string, secondValue: string) => void;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
& > input:last-child {
|
||||
padding-left: ${(props) => props.theme.spacing(2)};
|
||||
border-left: 1px solid ${(props) => props.theme.primaryBorder};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEditInplaceInput = styled.input`
|
||||
width: 45%;
|
||||
height: 18px;
|
||||
|
||||
${textInputStyle}
|
||||
`;
|
||||
|
||||
export function EditableDoubleText({
|
||||
firstValue,
|
||||
secondValue,
|
||||
firstValuePlaceholder,
|
||||
secondValuePlaceholder,
|
||||
nonEditModeContent,
|
||||
onChange,
|
||||
}: OwnProps) {
|
||||
const firstValueInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
onInsideClick={() => setIsEditMode(true)}
|
||||
onOutsideClick={() => setIsEditMode(false)}
|
||||
isEditMode={isEditMode}
|
||||
editModeContent={
|
||||
<StyledContainer>
|
||||
<StyledEditInplaceInput
|
||||
autoFocus
|
||||
placeholder={firstValuePlaceholder}
|
||||
ref={firstValueInputRef}
|
||||
value={firstValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(event.target.value, secondValue);
|
||||
}}
|
||||
/>
|
||||
<StyledEditInplaceInput
|
||||
placeholder={secondValuePlaceholder}
|
||||
ref={firstValueInputRef}
|
||||
value={secondValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(firstValue, event.target.value);
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
}
|
||||
nonEditModeContent={nonEditModeContent}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import { ChangeEvent, MouseEvent, useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
|
||||
|
||||
import Link from '../link/Link';
|
||||
|
||||
import { EditableCell } from './EditableCell';
|
||||
|
||||
type OwnProps = {
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
};
|
||||
|
||||
type StyledEditModeProps = {
|
||||
isEditMode: boolean;
|
||||
};
|
||||
|
||||
// TODO: refactor
|
||||
const StyledEditInplaceInput = styled.input<StyledEditModeProps>`
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
font-weight: bold;
|
||||
color: ${(props) => props.theme.text20};
|
||||
}
|
||||
`;
|
||||
|
||||
export function EditablePhone({ value, placeholder, changeHandler }: OwnProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={() => setIsEditMode(false)}
|
||||
onInsideClick={() => setIsEditMode(true)}
|
||||
editModeContent={
|
||||
<StyledEditInplaceInput
|
||||
autoFocus
|
||||
isEditMode={isEditMode}
|
||||
placeholder={placeholder || ''}
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
changeHandler(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={
|
||||
<div>
|
||||
{isValidPhoneNumber(inputValue) ? (
|
||||
<Link
|
||||
href={parsePhoneNumber(inputValue, 'FR')?.getURI()}
|
||||
onClick={(event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{parsePhoneNumber(inputValue, 'FR')?.formatInternational() ||
|
||||
inputValue}
|
||||
</Link>
|
||||
) : (
|
||||
<Link href="#">{inputValue}</Link>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,221 @@
|
||||
import { ChangeEvent, ComponentType, useEffect, useState } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { SearchConfigType } from '@/search/interfaces/interface';
|
||||
import { useSearch } from '@/search/services/search';
|
||||
import { AnyEntity } from '@/utils/interfaces/generic.interface';
|
||||
import { isDefined } from '@/utils/type-guards/isDefined';
|
||||
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
|
||||
|
||||
import { textInputStyle } from '../../layout/styles/themes';
|
||||
import { isSomeInputInEditModeState } from '../../tables/states/isSomeInputInEditModeState';
|
||||
|
||||
import { CellNormalModeContainer } from './CellNormalModeContainer';
|
||||
import { EditableCellMenu } from './EditableCellMenu';
|
||||
import { EditableRelationCreateButton } from './EditableRelationCreateButton';
|
||||
import { HoverableMenuItem } from './HoverableMenuItem';
|
||||
|
||||
const StyledEditModeContainer = styled.div`
|
||||
width: 200px;
|
||||
// margin-left: calc(-1 * ${(props) => props.theme.spacing(2)});
|
||||
// margin-right: calc(-1 * ${(props) => props.theme.spacing(2)});
|
||||
`;
|
||||
|
||||
const StyledEditModeSelectedContainer = styled.div`
|
||||
height: 31px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: ${(props) => props.theme.spacing(2)};
|
||||
padding-right: ${(props) => props.theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledEditModeSearchContainer = styled.div`
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 1px solid ${(props) => props.theme.primaryBorder};
|
||||
padding-left: ${(props) => props.theme.spacing(1)};
|
||||
padding-right: ${(props) => props.theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledEditModeCreateButtonContainer = styled.div`
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 1px solid ${(props) => props.theme.primaryBorder};
|
||||
padding: ${(props) => props.theme.spacing(1)};
|
||||
color: ${(props) => props.theme.text60};
|
||||
`;
|
||||
|
||||
const StyledEditModeSearchInput = styled.input`
|
||||
width: 100%;
|
||||
|
||||
${textInputStyle}
|
||||
`;
|
||||
|
||||
const StyledEditModeResults = styled.div`
|
||||
border-top: 1px solid ${(props) => props.theme.primaryBorder};
|
||||
padding-left: ${(props) => props.theme.spacing(1)};
|
||||
padding-right: ${(props) => props.theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledEditModeResultItem = styled.div`
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
const StyledCreateButtonIcon = styled.div`
|
||||
color: ${(props) => props.theme.text100};
|
||||
align-self: center;
|
||||
padding-top: 4px;
|
||||
`;
|
||||
|
||||
const StyledCreateButtonText = styled.div`
|
||||
color: ${(props) => props.theme.text60};
|
||||
`;
|
||||
|
||||
export type EditableRelationProps<
|
||||
RelationType extends AnyEntity,
|
||||
ChipComponentPropsType,
|
||||
> = {
|
||||
relation?: RelationType | null;
|
||||
searchPlaceholder: string;
|
||||
searchConfig: SearchConfigType<RelationType>;
|
||||
onChange: (relation: RelationType) => void;
|
||||
onChangeSearchInput?: (searchInput: string) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
ChipComponent: ComponentType<ChipComponentPropsType>;
|
||||
chipComponentPropsMapper: (
|
||||
relation: RelationType,
|
||||
) => ChipComponentPropsType & JSX.IntrinsicAttributes;
|
||||
// TODO: refactor, newRelationName is too hard coded.
|
||||
onCreate?: (newRelationName: string) => void;
|
||||
};
|
||||
|
||||
// TODO: split this component
|
||||
export function EditableRelation<
|
||||
RelationType extends AnyEntity,
|
||||
ChipComponentPropsType,
|
||||
>({
|
||||
relation,
|
||||
searchPlaceholder,
|
||||
searchConfig,
|
||||
onChange,
|
||||
onChangeSearchInput,
|
||||
editModeHorizontalAlign,
|
||||
ChipComponent,
|
||||
chipComponentPropsMapper,
|
||||
onCreate,
|
||||
}: EditableRelationProps<RelationType, ChipComponentPropsType>) {
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [, setIsSomeInputInEditMode] = useRecoilState(
|
||||
isSomeInputInEditModeState,
|
||||
);
|
||||
|
||||
// TODO: Tie this to a react context
|
||||
const [filterSearchResults, setSearchInput, setFilterSearch, searchInput] =
|
||||
useSearch<RelationType>();
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(onChangeSearchInput)) {
|
||||
onChangeSearchInput(searchInput);
|
||||
}
|
||||
}, [onChangeSearchInput, searchInput]);
|
||||
|
||||
const canCreate = isDefined(onCreate);
|
||||
|
||||
const createButtonIsVisible =
|
||||
canCreate && isEditMode && isNonEmptyString(searchInput);
|
||||
|
||||
function handleCreateNewRelationButtonClick() {
|
||||
onCreate?.(searchInput);
|
||||
closeEditMode();
|
||||
}
|
||||
|
||||
function closeEditMode() {
|
||||
setIsEditMode(false);
|
||||
setIsSomeInputInEditMode(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditableCellMenu
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={() => setIsEditMode(false)}
|
||||
onInsideClick={() => {
|
||||
if (!isEditMode) {
|
||||
setIsEditMode(true);
|
||||
}
|
||||
}}
|
||||
editModeContent={
|
||||
<StyledEditModeContainer>
|
||||
<StyledEditModeSelectedContainer>
|
||||
{relation ? (
|
||||
<ChipComponent {...chipComponentPropsMapper(relation)} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</StyledEditModeSelectedContainer>
|
||||
<StyledEditModeSearchContainer>
|
||||
<StyledEditModeSearchInput
|
||||
autoFocus
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterSearch(searchConfig);
|
||||
setSearchInput(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</StyledEditModeSearchContainer>
|
||||
{createButtonIsVisible && (
|
||||
<StyledEditModeCreateButtonContainer>
|
||||
<HoverableMenuItem>
|
||||
<EditableRelationCreateButton
|
||||
onClick={handleCreateNewRelationButtonClick}
|
||||
>
|
||||
<StyledCreateButtonIcon>
|
||||
<FaPlus />
|
||||
</StyledCreateButtonIcon>
|
||||
<StyledCreateButtonText>Create new</StyledCreateButtonText>
|
||||
</EditableRelationCreateButton>
|
||||
</HoverableMenuItem>
|
||||
</StyledEditModeCreateButtonContainer>
|
||||
)}
|
||||
<StyledEditModeResults>
|
||||
{filterSearchResults.results &&
|
||||
filterSearchResults.results.map((result, index) => (
|
||||
<StyledEditModeResultItem
|
||||
key={index}
|
||||
onClick={() => {
|
||||
onChange(result.value);
|
||||
closeEditMode();
|
||||
}}
|
||||
>
|
||||
<HoverableMenuItem>
|
||||
<ChipComponent
|
||||
{...chipComponentPropsMapper(result.value)}
|
||||
/>
|
||||
</HoverableMenuItem>
|
||||
</StyledEditModeResultItem>
|
||||
))}
|
||||
</StyledEditModeResults>
|
||||
</StyledEditModeContainer>
|
||||
}
|
||||
nonEditModeContent={
|
||||
<CellNormalModeContainer>
|
||||
{relation ? (
|
||||
<ChipComponent {...chipComponentPropsMapper(relation)} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</CellNormalModeContainer>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const EditableRelationCreateButton = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
font-size: ${(props) => props.theme.fontSizeMedium};
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding-top: ${(props) => props.theme.spacing(1)};
|
||||
padding-bottom: ${(props) => props.theme.spacing(1)};
|
||||
padding-left: ${(props) => props.theme.spacing(1)};
|
||||
font-family: 'Inter';
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
height: 31px;
|
||||
background: none;
|
||||
gap: 8px;
|
||||
// :hover {
|
||||
// background: rgba(0, 0, 0, 0.04);
|
||||
// color: ${(props) => props.theme.text100};
|
||||
// }
|
||||
// margin-bottom: calc(${(props) => props.theme.spacing(1)} / 2);
|
||||
`;
|
||||
@ -0,0 +1,61 @@
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { textInputStyle } from '../../layout/styles/themes';
|
||||
|
||||
import { EditableCell } from './EditableCell';
|
||||
|
||||
type OwnProps = {
|
||||
placeholder?: string;
|
||||
content: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
editModeHorizontalAlign?: 'left' | 'right';
|
||||
};
|
||||
|
||||
type StyledEditModeProps = {
|
||||
isEditMode: boolean;
|
||||
};
|
||||
|
||||
// TODO: refactor
|
||||
const StyledInplaceInput = styled.input<StyledEditModeProps>`
|
||||
width: 100%;
|
||||
${textInputStyle}
|
||||
`;
|
||||
|
||||
const StyledNoEditText = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function EditableText({
|
||||
content,
|
||||
placeholder,
|
||||
changeHandler,
|
||||
editModeHorizontalAlign,
|
||||
}: OwnProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(content);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
return (
|
||||
<EditableCell
|
||||
isEditMode={isEditMode}
|
||||
onOutsideClick={() => setIsEditMode(false)}
|
||||
onInsideClick={() => setIsEditMode(true)}
|
||||
editModeHorizontalAlign={editModeHorizontalAlign}
|
||||
editModeContent={
|
||||
<StyledInplaceInput
|
||||
isEditMode={isEditMode}
|
||||
placeholder={placeholder || ''}
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
changeHandler(event.target.value);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={<StyledNoEditText>{inputValue}</StyledNoEditText>}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const HoverableMenuItem = styled.div`
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
transition: background 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user