Uniformize folder structure (#693)
* Uniformize folder structure * Fix icons * Fix icons * Fix tests * Fix tests
This commit is contained in:
133
front/src/modules/ui/editable-field/components/EditableField.tsx
Normal file
133
front/src/modules/ui/editable-field/components/EditableField.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
|
||||
|
||||
import { useEditableField } from '../hooks/useEditableField';
|
||||
|
||||
import { EditableFieldDisplayMode } from './EditableFieldDisplayMode';
|
||||
import { EditableFieldEditButton } from './EditableFieldEditButton';
|
||||
import { EditableFieldEditMode } from './EditableFieldEditMode';
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
|
||||
svg {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLabelAndIconContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.div<Pick<OwnProps, 'labelFixedWidth'>>`
|
||||
align-items: center;
|
||||
|
||||
width: ${({ labelFixedWidth }) =>
|
||||
labelFixedWidth ? `${labelFixedWidth}px` : 'fit-content'};
|
||||
`;
|
||||
|
||||
export const EditableFieldBaseContainer = styled.div`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: 24px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
iconLabel?: React.ReactNode;
|
||||
label?: string;
|
||||
labelFixedWidth?: number;
|
||||
useEditButton?: boolean;
|
||||
editModeContent: React.ReactNode;
|
||||
displayModeContent: React.ReactNode;
|
||||
parentHotkeyScope?: HotkeyScope;
|
||||
customEditHotkeyScope?: HotkeyScope;
|
||||
onSubmit?: () => void;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
export function EditableField({
|
||||
iconLabel,
|
||||
label,
|
||||
labelFixedWidth,
|
||||
useEditButton,
|
||||
editModeContent,
|
||||
displayModeContent,
|
||||
parentHotkeyScope,
|
||||
customEditHotkeyScope,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: OwnProps) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
function handleContainerMouseEnter() {
|
||||
setIsHovered(true);
|
||||
}
|
||||
|
||||
function handleContainerMouseLeave() {
|
||||
setIsHovered(false);
|
||||
}
|
||||
|
||||
const { isFieldInEditMode, openEditableField } =
|
||||
useEditableField(parentHotkeyScope);
|
||||
|
||||
function handleDisplayModeClick() {
|
||||
openEditableField(customEditHotkeyScope);
|
||||
}
|
||||
|
||||
const showEditButton = !isFieldInEditMode && isHovered && useEditButton;
|
||||
|
||||
return (
|
||||
<EditableFieldBaseContainer
|
||||
onMouseEnter={handleContainerMouseEnter}
|
||||
onMouseLeave={handleContainerMouseLeave}
|
||||
>
|
||||
<StyledLabelAndIconContainer>
|
||||
{iconLabel && <StyledIconContainer>{iconLabel}</StyledIconContainer>}
|
||||
{label && (
|
||||
<StyledLabel labelFixedWidth={labelFixedWidth}>{label}</StyledLabel>
|
||||
)}
|
||||
</StyledLabelAndIconContainer>
|
||||
{isFieldInEditMode ? (
|
||||
<EditableFieldEditMode onSubmit={onSubmit} onCancel={onCancel}>
|
||||
{editModeContent}
|
||||
</EditableFieldEditMode>
|
||||
) : (
|
||||
<EditableFieldDisplayMode
|
||||
disableClick={useEditButton}
|
||||
onClick={handleDisplayModeClick}
|
||||
>
|
||||
{displayModeContent}
|
||||
</EditableFieldDisplayMode>
|
||||
)}
|
||||
{showEditButton && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
whileHover={{ scale: 1.04 }}
|
||||
>
|
||||
<EditableFieldEditButton customHotkeyScope={customEditHotkeyScope} />
|
||||
</motion.div>
|
||||
)}
|
||||
</EditableFieldBaseContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
export const EditableFieldNormalModeOuterContainer = styled.div<
|
||||
Pick<OwnProps, 'disableClick'>
|
||||
>`
|
||||
align-items: center;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
height: 16px;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
|
||||
width: 100%;
|
||||
|
||||
${(props) => {
|
||||
if (props.disableClick) {
|
||||
return css`
|
||||
cursor: default;
|
||||
`;
|
||||
} else {
|
||||
return css`
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: ${props.theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
|
||||
export const EditableFieldNormalModeInnerContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
|
||||
height: fit-content;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
disableClick?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export function EditableFieldDisplayMode({
|
||||
children,
|
||||
disableClick,
|
||||
onClick,
|
||||
}: React.PropsWithChildren<OwnProps>) {
|
||||
return (
|
||||
<EditableFieldNormalModeOuterContainer
|
||||
onClick={disableClick ? undefined : onClick}
|
||||
disableClick={disableClick}
|
||||
>
|
||||
<EditableFieldNormalModeInnerContainer>
|
||||
{children}
|
||||
</EditableFieldNormalModeInnerContainer>
|
||||
</EditableFieldNormalModeOuterContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconButton } from '@/ui/button/components/IconButton';
|
||||
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
|
||||
import { IconPencil } from '@/ui/icon';
|
||||
import { overlayBackground } from '@/ui/themes/effects';
|
||||
|
||||
import { useEditableField } from '../hooks/useEditableField';
|
||||
|
||||
export const StyledEditableFieldEditButton = styled.div`
|
||||
align-items: center;
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 20px;
|
||||
justify-content: center;
|
||||
|
||||
margin-left: -2px;
|
||||
width: 20px;
|
||||
|
||||
z-index: 1;
|
||||
${overlayBackground}
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
customHotkeyScope?: HotkeyScope;
|
||||
};
|
||||
|
||||
export function EditableFieldEditButton({ customHotkeyScope }: OwnProps) {
|
||||
const { openEditableField } = useEditableField();
|
||||
|
||||
function handleClick() {
|
||||
openEditableField(customHotkeyScope);
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant="shadow"
|
||||
size="small"
|
||||
onClick={handleClick}
|
||||
icon={<IconPencil size={14} />}
|
||||
data-testid="editable-field-edit-mode-container"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import { useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers';
|
||||
|
||||
export const EditableFieldEditModeContainer = styled.div<OwnProps>`
|
||||
align-items: center;
|
||||
|
||||
display: flex;
|
||||
|
||||
margin-left: -${({ theme }) => theme.spacing(1)};
|
||||
|
||||
width: inherit;
|
||||
z-index: 10;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
children: React.ReactNode;
|
||||
onOutsideClick?: () => void;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: () => void;
|
||||
};
|
||||
|
||||
export function EditableFieldEditMode({
|
||||
children,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: OwnProps) {
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
useRegisterCloseFieldHandlers(wrapperRef, onSubmit, onCancel);
|
||||
|
||||
return (
|
||||
<EditableFieldEditModeContainer
|
||||
data-testid="editable-field-edit-mode-container"
|
||||
ref={wrapperRef}
|
||||
>
|
||||
{children}
|
||||
</EditableFieldEditModeContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { EditableField } from '@/ui/editable-field/components/EditableField';
|
||||
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
|
||||
import { IconMap } from '@/ui/icon';
|
||||
import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText';
|
||||
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
|
||||
import { Company, useUpdateCompanyMutation } from '~/generated/graphql';
|
||||
|
||||
type OwnProps = {
|
||||
company: Pick<Company, 'id' | 'address'>;
|
||||
};
|
||||
|
||||
export function CompanyEditableFieldAddress({ company }: OwnProps) {
|
||||
const [internalValue, setInternalValue] = useState(company.address);
|
||||
|
||||
const [updateCompany] = useUpdateCompanyMutation();
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(company.address);
|
||||
}, [company.address]);
|
||||
|
||||
async function handleChange(newValue: string) {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await updateCompany({
|
||||
variables: {
|
||||
id: company.id,
|
||||
address: internalValue ?? '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCancel() {
|
||||
setInternalValue(company.address);
|
||||
}
|
||||
|
||||
return (
|
||||
<RecoilScope SpecificContext={FieldContext}>
|
||||
<EditableField
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
iconLabel={<IconMap />}
|
||||
editModeContent={
|
||||
<InplaceInputText
|
||||
placeholder={'Address'}
|
||||
autoFocus
|
||||
value={internalValue}
|
||||
onChange={(newValue: string) => {
|
||||
handleChange(newValue);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
displayModeContent={internalValue !== '' ? internalValue : 'No address'}
|
||||
/>
|
||||
</RecoilScope>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import { RawLink } from '@/ui/link/components/RawLink';
|
||||
|
||||
export function FieldDisplayURL({ URL }: { URL: string | undefined }) {
|
||||
return <RawLink href={URL ? 'https://' + URL : ''}>{URL}</RawLink>;
|
||||
}
|
||||
Reference in New Issue
Block a user