mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-18 09:02:11 +03:00
Update Edit Inplace behavior and style (#97)
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
parent
3605c0034c
commit
e65fd3d6a5
@ -1,87 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
|
||||
type OwnProps = {
|
||||
content: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
};
|
||||
|
||||
const StyledEditable = styled.div`
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
:hover::before {
|
||||
display: block;
|
||||
}
|
||||
|
||||
::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
width: calc(100% + 2px);
|
||||
height: calc(100% + 2px);
|
||||
border: 1px solid ${(props) => props.theme.text20};
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:has(input:focus-within)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
width: calc(100% + 2px);
|
||||
height: calc(100% + 2px);
|
||||
border: 1px solid ${(props) => props.theme.text20};
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.09);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInplaceInput = styled.input`
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
padding-left: ${(props) => props.theme.spacing(2)};
|
||||
padding-right: ${(props) => props.theme.spacing(2)};
|
||||
`;
|
||||
|
||||
function EditableCell({ content, changeHandler }: OwnProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(content);
|
||||
|
||||
return (
|
||||
<StyledEditable
|
||||
onClick={() => {
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
<StyledInplaceInput
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
changeHandler(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
</StyledEditable>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableCell;
|
@ -1,31 +0,0 @@
|
||||
import EditableCell from '../EditableCell';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { lightTheme } from '../../../layout/styles/themes';
|
||||
import { StoryFn } from '@storybook/react';
|
||||
|
||||
const component = {
|
||||
title: 'EditableCell',
|
||||
component: EditableCell,
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
content: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
};
|
||||
|
||||
export default component;
|
||||
|
||||
const Template: StoryFn<typeof EditableCell> = (args: OwnProps) => {
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<div data-testid="content-editable-parent">
|
||||
<EditableCell {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditableCellStory = Template.bind({});
|
||||
EditableCellStory.args = {
|
||||
content: 'Test string',
|
||||
};
|
@ -0,0 +1,71 @@
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { ReactElement, useRef, useState } from 'react';
|
||||
import { useOutsideAlerter } from '../../../hooks/useOutsideAlerter';
|
||||
import { ThemeType } from '../../../layout/styles/themes';
|
||||
|
||||
type OwnProps = {
|
||||
children: ReactElement;
|
||||
onEditModeChange: (isEditMode: boolean) => void;
|
||||
};
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type styledEditModeWrapperProps = {
|
||||
isEditMode: boolean;
|
||||
};
|
||||
|
||||
const styledEditModeWrapper = (theme: ThemeType) =>
|
||||
css`
|
||||
position: absolute;
|
||||
width: 260px;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
padding-left: ${theme.spacing(2)};
|
||||
padding-right: ${theme.spacing(2)};
|
||||
background: ${theme.primaryBackground};
|
||||
border: 1px solid ${theme.primaryBorder};
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.16);
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
backdrop-filter: blur(20px);
|
||||
`;
|
||||
|
||||
const Container = styled.div<styledEditModeWrapperProps>`
|
||||
width: 100%;
|
||||
padding-left: ${(props) => props.theme.spacing(2)};
|
||||
padding-right: ${(props) => props.theme.spacing(2)};
|
||||
${(props) => props.isEditMode && styledEditModeWrapper(props.theme)}
|
||||
`;
|
||||
|
||||
function EditableCellWrapper({ children, onEditModeChange }: OwnProps) {
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
const wrapperRef = useRef(null);
|
||||
useOutsideAlerter(wrapperRef, () => {
|
||||
setIsEditMode(false);
|
||||
onEditModeChange(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
ref={wrapperRef}
|
||||
onClick={() => {
|
||||
setIsEditMode(true);
|
||||
onEditModeChange(true);
|
||||
}}
|
||||
>
|
||||
<Container isEditMode={isEditMode}>{children}</Container>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableCellWrapper;
|
55
front/src/components/table/editable-cell/EditableText.tsx
Normal file
55
front/src/components/table/editable-cell/EditableText.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { ChangeEvent, useRef, useState } from 'react';
|
||||
import EditableCellWrapper from './EditableCellWrapper';
|
||||
|
||||
type OwnProps = {
|
||||
placeholder?: string;
|
||||
content: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
};
|
||||
|
||||
type StyledEditModeProps = {
|
||||
isEditMode: boolean;
|
||||
};
|
||||
|
||||
const StyledInplaceInput = styled.input<StyledEditModeProps>`
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
font-weight: ${(props) => (props.isEditMode ? 'bold' : 'normal')};
|
||||
color: ${(props) =>
|
||||
props.isEditMode ? props.theme.text20 : 'transparent'};
|
||||
}
|
||||
`;
|
||||
|
||||
function EditableCell({ content, placeholder, changeHandler }: OwnProps) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputValue, setInputValue] = useState(content);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
const onEditModeChange = (isEditMode: boolean) => {
|
||||
setIsEditMode(isEditMode);
|
||||
if (isEditMode) {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EditableCellWrapper onEditModeChange={onEditModeChange}>
|
||||
<StyledInplaceInput
|
||||
isEditMode={isEditMode}
|
||||
placeholder={placeholder || ''}
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(event.target.value);
|
||||
changeHandler(event.target.value);
|
||||
}}
|
||||
/>
|
||||
</EditableCellWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditableCell;
|
@ -0,0 +1,35 @@
|
||||
import EditableText from '../EditableText';
|
||||
import { ThemeProvider } from '@emotion/react';
|
||||
import { lightTheme } from '../../../../layout/styles/themes';
|
||||
import { StoryFn } from '@storybook/react';
|
||||
|
||||
const component = {
|
||||
title: 'EditableText',
|
||||
component: EditableText,
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
content: string;
|
||||
changeHandler: (updated: string) => void;
|
||||
};
|
||||
|
||||
export default component;
|
||||
|
||||
const Template: StoryFn<typeof EditableText> = (args: OwnProps) => {
|
||||
return (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<div data-testid="content-editable-parent">
|
||||
<EditableText {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditableTextStory = Template.bind({});
|
||||
EditableTextStory.args = {
|
||||
placeholder: 'Test placeholder',
|
||||
content: 'Test string',
|
||||
changeHandler: () => {
|
||||
console.log('changed');
|
||||
},
|
||||
};
|
@ -1,14 +1,22 @@
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
|
||||
import { EditableCellStory } from '../__stories__/EditableCell.stories';
|
||||
import { EditableTextStory } from '../__stories__/EditableText.stories';
|
||||
|
||||
it('Checks the EditableCell editing event bubbles up', async () => {
|
||||
const func = jest.fn(() => null);
|
||||
const { getByTestId } = render(
|
||||
<EditableCellStory content="test" changeHandler={func} />,
|
||||
<EditableTextStory content="test" changeHandler={func} />,
|
||||
);
|
||||
|
||||
const parent = getByTestId('content-editable-parent');
|
||||
|
||||
const wrapper = parent.querySelector('div');
|
||||
|
||||
if (!wrapper) {
|
||||
throw new Error('Editable input not found');
|
||||
}
|
||||
fireEvent.click(wrapper);
|
||||
|
||||
const editableInput = parent.querySelector('input');
|
||||
|
||||
if (!editableInput) {
|
@ -5,7 +5,7 @@ import ColumnHead from '../../components/table/ColumnHead';
|
||||
import HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer';
|
||||
import Checkbox from '../../components/form/Checkbox';
|
||||
import CompanyChip from '../../components/chips/CompanyChip';
|
||||
import EditableCell from '../../components/table/EditableCell';
|
||||
import EditableText from '../../components/table/editable-cell/EditableText';
|
||||
import PipeChip from '../../components/chips/PipeChip';
|
||||
import {
|
||||
FaRegBuilding,
|
||||
@ -53,7 +53,7 @@ export const companiesColumns = [
|
||||
columnHelper.accessor('employees', {
|
||||
header: () => <ColumnHead viewName="Employees" viewIcon={<FaUsers />} />,
|
||||
cell: (props) => (
|
||||
<EditableCell
|
||||
<EditableText
|
||||
content={props.row.original.employees.toFixed(0)}
|
||||
changeHandler={(value) => {
|
||||
const company = props.row.original;
|
||||
@ -66,7 +66,7 @@ export const companiesColumns = [
|
||||
columnHelper.accessor('domain_name', {
|
||||
header: () => <ColumnHead viewName="URL" viewIcon={<FaLink />} />,
|
||||
cell: (props) => (
|
||||
<EditableCell
|
||||
<EditableText
|
||||
content={props.row.original.domain_name}
|
||||
changeHandler={(value) => {
|
||||
const company = props.row.original;
|
||||
@ -79,7 +79,7 @@ export const companiesColumns = [
|
||||
columnHelper.accessor('address', {
|
||||
header: () => <ColumnHead viewName="Address" viewIcon={<FaMapPin />} />,
|
||||
cell: (props) => (
|
||||
<EditableCell
|
||||
<EditableText
|
||||
content={props.row.original.address}
|
||||
changeHandler={(value) => {
|
||||
const company = props.row.original;
|
||||
|
@ -19,7 +19,7 @@ import CompanyChip from '../../components/chips/CompanyChip';
|
||||
import PersonChip from '../../components/chips/PersonChip';
|
||||
import { GraphqlQueryPerson, Person } from '../../interfaces/person.interface';
|
||||
import PipeChip from '../../components/chips/PipeChip';
|
||||
import EditableCell from '../../components/table/EditableCell';
|
||||
import EditableText from '../../components/table/editable-cell/EditableText';
|
||||
import { OrderByFields, updatePerson } from '../../services/people';
|
||||
import {
|
||||
FilterType,
|
||||
@ -152,7 +152,8 @@ export const peopleColumns = [
|
||||
columnHelper.accessor('email', {
|
||||
header: () => <ColumnHead viewName="Email" viewIcon={<FaEnvelope />} />,
|
||||
cell: (props) => (
|
||||
<EditableCell
|
||||
<EditableText
|
||||
placeholder="Email"
|
||||
content={props.row.original.email}
|
||||
changeHandler={(value) => {
|
||||
const person = props.row.original;
|
||||
|
Loading…
Reference in New Issue
Block a user