Create Editable chip to edit cells containing chips (#102)
* Create Editable chip to edit cells containing chips * Fix icons vertical alignment * Fix linter * Fix coverage
This commit is contained in:
@ -55,7 +55,8 @@
|
|||||||
"coveragePathIgnorePatterns": [
|
"coveragePathIgnorePatterns": [
|
||||||
".stories.tsx$",
|
".stories.tsx$",
|
||||||
"graphql.tsx$",
|
"graphql.tsx$",
|
||||||
"apollo.tsx$"
|
"apollo.tsx$",
|
||||||
|
"src/index.tsx$"
|
||||||
],
|
],
|
||||||
"coverageThreshold": {
|
"coverageThreshold": {
|
||||||
"global": {
|
"global": {
|
||||||
|
|||||||
69
front/src/components/table/editable-cell/EditableChip.tsx
Normal file
69
front/src/components/table/editable-cell/EditableChip.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { ChangeEvent, ComponentType, useRef, useState } from 'react';
|
||||||
|
import EditableCellWrapper from './EditableCellWrapper';
|
||||||
|
|
||||||
|
export type EditableChipProps = {
|
||||||
|
placeholder?: string;
|
||||||
|
value: string;
|
||||||
|
picture: string;
|
||||||
|
changeHandler: (updated: string) => void;
|
||||||
|
shouldAlignRight?: boolean;
|
||||||
|
ChipComponent: ComponentType<{ name: string; picture: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 EditableChip({
|
||||||
|
value,
|
||||||
|
placeholder,
|
||||||
|
changeHandler,
|
||||||
|
picture,
|
||||||
|
shouldAlignRight,
|
||||||
|
ChipComponent,
|
||||||
|
}: EditableChipProps) {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [inputValue, setInputValue] = useState(value);
|
||||||
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
|
||||||
|
const onEditModeChange = (isEditMode: boolean) => {
|
||||||
|
setIsEditMode(isEditMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditableCellWrapper
|
||||||
|
onEditModeChange={onEditModeChange}
|
||||||
|
shouldAlignRight={shouldAlignRight}
|
||||||
|
>
|
||||||
|
{isEditMode ? (
|
||||||
|
<StyledInplaceInput
|
||||||
|
isEditMode={isEditMode}
|
||||||
|
placeholder={placeholder || ''}
|
||||||
|
autoFocus
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setInputValue(event.target.value);
|
||||||
|
changeHandler(event.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChipComponent name={value} picture={picture} />
|
||||||
|
)}
|
||||||
|
</EditableCellWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditableChip;
|
||||||
@ -29,7 +29,7 @@ const StyledNoEditText = styled.div`
|
|||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function EditableCell({
|
function EditableText({
|
||||||
content,
|
content,
|
||||||
placeholder,
|
placeholder,
|
||||||
changeHandler,
|
changeHandler,
|
||||||
@ -67,4 +67,4 @@ function EditableCell({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditableCell;
|
export default EditableText;
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
import EditableChip, { EditableChipProps } from '../EditableChip';
|
||||||
|
import { ThemeProvider } from '@emotion/react';
|
||||||
|
import { lightTheme } from '../../../../layout/styles/themes';
|
||||||
|
import { StoryFn } from '@storybook/react';
|
||||||
|
import CompanyChip from '../../../chips/CompanyChip';
|
||||||
|
|
||||||
|
const component = {
|
||||||
|
title: 'EditableChip',
|
||||||
|
component: EditableChip,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default component;
|
||||||
|
|
||||||
|
const Template: StoryFn<typeof EditableChip> = (args: EditableChipProps) => {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<div data-testid="content-editable-parent">
|
||||||
|
<EditableChip {...args} />
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditableChipStory = Template.bind({});
|
||||||
|
EditableChipStory.args = {
|
||||||
|
ChipComponent: CompanyChip,
|
||||||
|
placeholder: 'Test',
|
||||||
|
value: 'Test',
|
||||||
|
picture: 'https://picsum.photos/200',
|
||||||
|
changeHandler: () => {
|
||||||
|
console.log('changed');
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { EditableChipStory } from '../__stories__/EditableChip.stories';
|
||||||
|
import CompanyChip from '../../../chips/CompanyChip';
|
||||||
|
|
||||||
|
it('Checks the EditableChip editing event bubbles up', async () => {
|
||||||
|
const func = jest.fn(() => null);
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<EditableChipStory
|
||||||
|
value="test"
|
||||||
|
picture="http://"
|
||||||
|
changeHandler={func}
|
||||||
|
ChipComponent={CompanyChip}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
throw new Error('Editable input not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent.change(editableInput, { target: { value: 'Test' } });
|
||||||
|
expect(func).toBeCalledWith('Test');
|
||||||
|
});
|
||||||
@ -23,11 +23,15 @@ const StyledChip = styled.div`
|
|||||||
`;
|
`;
|
||||||
const StyledIcon = styled.div`
|
const StyledIcon = styled.div`
|
||||||
margin-right: ${(props) => props.theme.spacing(1)};
|
margin-right: ${(props) => props.theme.spacing(1)};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledDelete = styled.div`
|
const StyledDelete = styled.div`
|
||||||
margin-left: ${(props) => props.theme.spacing(2)};
|
margin-left: ${(props) => props.theme.spacing(2)};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledLabelKey = styled.div`
|
const StyledLabelKey = styled.div`
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
type OwnProps = {
|
|
||||||
children: JSX.Element[] | JSX.Element;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function HorizontalyAlignedContainer({ children }: OwnProps) {
|
|
||||||
return <StyledContainer>{children}</StyledContainer>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HorizontalyAlignedContainer;
|
|
||||||
@ -15,6 +15,7 @@ type StyledItemProps = {
|
|||||||
|
|
||||||
const StyledItem = styled.button<StyledItemProps>`
|
const StyledItem = styled.button<StyledItemProps>`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: ${(props) => props.theme.fontSizeMedium};
|
font-size: ${(props) => props.theme.fontSizeMedium};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { createColumnHelper } from '@tanstack/react-table';
|
|||||||
import { Company } from '../../interfaces/company.interface';
|
import { Company } from '../../interfaces/company.interface';
|
||||||
import { OrderByFields, updateCompany } from '../../services/companies';
|
import { OrderByFields, updateCompany } from '../../services/companies';
|
||||||
import ColumnHead from '../../components/table/ColumnHead';
|
import ColumnHead from '../../components/table/ColumnHead';
|
||||||
import HorizontalyAlignedContainer from '../../layout/containers/HorizontalyAlignedContainer';
|
|
||||||
import Checkbox from '../../components/form/Checkbox';
|
import Checkbox from '../../components/form/Checkbox';
|
||||||
import CompanyChip from '../../components/chips/CompanyChip';
|
import CompanyChip from '../../components/chips/CompanyChip';
|
||||||
import EditableText from '../../components/table/editable-cell/EditableText';
|
import EditableText from '../../components/table/editable-cell/EditableText';
|
||||||
@ -19,6 +18,7 @@ import {
|
|||||||
import ClickableCell from '../../components/table/ClickableCell';
|
import ClickableCell from '../../components/table/ClickableCell';
|
||||||
import PersonChip from '../../components/chips/PersonChip';
|
import PersonChip from '../../components/chips/PersonChip';
|
||||||
import { SortType } from '../../components/table/table-header/interface';
|
import { SortType } from '../../components/table/table-header/interface';
|
||||||
|
import EditableChip from '../../components/table/editable-cell/EditableChip';
|
||||||
|
|
||||||
export const sortsAvailable = [
|
export const sortsAvailable = [
|
||||||
{
|
{
|
||||||
@ -35,19 +35,31 @@ export const sortsAvailable = [
|
|||||||
|
|
||||||
const columnHelper = createColumnHelper<Company>();
|
const columnHelper = createColumnHelper<Company>();
|
||||||
export const companiesColumns = [
|
export const companiesColumns = [
|
||||||
|
columnHelper.accessor('id', {
|
||||||
|
header: () => (
|
||||||
|
<Checkbox id="company-select-all" name="company-select-all" />
|
||||||
|
),
|
||||||
|
cell: (props) => (
|
||||||
|
<Checkbox
|
||||||
|
id={`company-selected-${props.row.original.id}`}
|
||||||
|
name={`company-selected-${props.row.original.id}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
columnHelper.accessor('name', {
|
columnHelper.accessor('name', {
|
||||||
header: () => <ColumnHead viewName="Name" viewIcon={<FaRegBuilding />} />,
|
header: () => <ColumnHead viewName="Name" viewIcon={<FaRegBuilding />} />,
|
||||||
cell: (props) => (
|
cell: (props) => (
|
||||||
<HorizontalyAlignedContainer>
|
<EditableChip
|
||||||
<Checkbox
|
value={props.row.original.name}
|
||||||
id={`company-selected-${props.row.original.id}`}
|
placeholder="Name"
|
||||||
name={`company-selected-${props.row.original.id}`}
|
picture={`https://www.google.com/s2/favicons?domain=${props.row.original.domain_name}&sz=256`}
|
||||||
/>
|
changeHandler={(value: string) => {
|
||||||
<CompanyChip
|
const company = props.row.original;
|
||||||
name={props.row.original.name}
|
company.name = value;
|
||||||
picture={`https://www.google.com/s2/favicons?domain=${props.row.original.domain_name}&sz=256`}
|
updateCompany(company).catch((error) => console.error(error));
|
||||||
/>
|
}}
|
||||||
</HorizontalyAlignedContainer>
|
ChipComponent={CompanyChip}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('employees', {
|
columnHelper.accessor('employees', {
|
||||||
|
|||||||
Reference in New Issue
Block a user