Add "show company / people" view and "Notes" concept (#528)

* Begin adding show view and refactoring threads to become notes

* Progress on design

* Progress redesign timeline

* Dropdown button, design improvement

* Open comment thread edit mode in drawer

* Autosave local storage and commentThreadcount

* Improve display and fix missing key issue

* Remove some hardcoded CSS properties

* Create button

* Split company show into ui/business + fix eslint

* Fix font weight

* Begin auto-save on edit mode

* Save server-side query result to Apollo cache

* Fix save behavior

* Refetch timeline after creating note

* Rename createCommentThreadWithComment

* Improve styling

* Revert "Improve styling"

This reverts commit 9fbbf2db006e529330edc64f3eb8ff9ecdde6bb0.

* Improve CSS styling

* Bring back border radius inadvertently removed

* padding adjustment

* Improve blocknote design

* Improve edit mode display

* Remove Comments.tsx

* Remove irrelevant comment stories

* Removed un-necessary panel component

* stop using fragment, move trash icon

* Add a basic story for CompanyShow

* Add a basic People show view

* Fix storybook tests

* Add very basic Person story

* Refactor PR1

* Refactor part 2

* Refactor part 3

* Refactor part 4

* Fix tests

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Félix Malfait
2023-07-10 07:25:34 +02:00
committed by GitHub
parent ca180acf9f
commit 94a913a41f
191 changed files with 5390 additions and 721 deletions

View File

@ -0,0 +1,94 @@
import React, { useState } from 'react';
import styled from '@emotion/styled';
import { IconChevronDown } from '@/ui/icons/index';
type ButtonProps = React.ComponentProps<'button'>;
export type DropdownOptionType = {
label: string;
icon: React.ReactNode;
};
type Props = {
options: DropdownOptionType[];
onSelection: (value: DropdownOptionType) => void;
} & ButtonProps;
const StyledButton = styled.button<ButtonProps>`
align-items: center;
background: ${({ theme }) => theme.background.tertiary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: 4px;
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: 8px;
height: 24px;
line-height: ${({ theme }) => theme.text.lineHeight.lg};
padding: 3px 8px;
svg {
align-items: center;
display: flex;
height: 14px;
justify-content: center;
width: 14px;
}
`;
const DropdownContainer = styled.div`
position: relative;
`;
const DropdownMenu = styled.div`
display: flex;
flex-direction: column;
margin-top: -2px;
position: absolute;
`;
export function DropdownButton({
options,
onSelection,
...buttonProps
}: Props) {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState(options[0]);
if (!options.length) {
throw new Error('You must provide at least one option.');
}
const handleSelect =
(option: DropdownOptionType) =>
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
onSelection(option);
setSelectedOption(option);
setIsOpen(false);
};
return (
<DropdownContainer>
<StyledButton onClick={() => setIsOpen(!isOpen)} {...buttonProps}>
{selectedOption.icon}
{selectedOption.label}
{options.length > 1 && <IconChevronDown />}
</StyledButton>
{isOpen && (
<DropdownMenu>
{options
.filter((option) => option.label !== selectedOption.label)
.map((option, index) => (
<StyledButton key={index} onClick={handleSelect(option)}>
{option.icon}
{option.label}
</StyledButton>
))}
</DropdownMenu>
)}
</DropdownContainer>
);
}

View File

@ -6,17 +6,19 @@ import { textInputStyle } from '@/ui/themes/effects';
import { EditableCell } from '../EditableCell';
export type EditableChipProps = {
id: string;
placeholder?: string;
value: string;
picture: string;
changeHandler: (updated: string) => void;
editModeHorizontalAlign?: 'left' | 'right';
ChipComponent: ComponentType<{
id: string;
name: string;
picture: string;
isOverlapped?: boolean;
}>;
commentCount?: number;
commentThreadCount?: number;
onCommentClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
rightEndContents?: ReactNode[];
};
@ -41,6 +43,7 @@ const RightContainer = styled.div`
// TODO: move right end content in EditableCell
export function EditableCellChip({
id,
value,
placeholder,
changeHandler,
@ -75,7 +78,7 @@ export function EditableCellChip({
}
nonEditModeContent={
<NoEditModeContainer>
<ChipComponent name={inputValue} picture={picture} />
<ChipComponent id={id} name={inputValue} picture={picture} />
<RightContainer>
{rightEndContents &&
rightEndContents.length > 0 &&

View File

@ -0,0 +1,28 @@
import { BlockNoteEditor } from '@blocknote/core';
import { BlockNoteView } from '@blocknote/react';
import styled from '@emotion/styled';
interface BlockEditorProps {
editor: BlockNoteEditor | null;
}
const StyledEditor = styled.div`
min-height: 200px;
width: 100%;
& .editor-create-mode,
.editor-edit-mode {
background: ${({ theme }) => theme.background.primary};
}
& .editor-create-mode [class^='_inlineContent']:before {
color: ${({ theme }) => theme.font.color.tertiary};
font-style: normal !important;
}
`;
export function BlockEditor({ editor }: BlockEditorProps) {
return (
<StyledEditor>
<BlockNoteView editor={editor} />
</StyledEditor>
);
}

View File

@ -22,7 +22,7 @@ const StyledContainer = styled.div`
font-size: ${({ theme }) => theme.font.size.md};
border: none;
display: block;
font-weight: 400;
font-weight: ${({ theme }) => theme.font.weight.regular};
}
& .react-datepicker-popper {

View File

@ -27,8 +27,8 @@ const StyledTextArea = styled(TextareaAutosize)`
border-radius: 5px;
color: ${({ theme }) => theme.font.color.primary};
font-family: inherit;
font-size: 13px;
font-weight: 400;
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
line-height: 16px;
overflow: auto;
padding: 8px;
@ -42,7 +42,7 @@ const StyledTextArea = styled(TextareaAutosize)`
&::placeholder {
color: ${({ theme }) => theme.font.color.light};
font-weight: 400;
font-weight: ${({ theme }) => theme.font.weight.regular};
}
`;
@ -121,7 +121,7 @@ export function AutosizeTextInput({
<>
<StyledContainer>
<StyledTextArea
placeholder={placeholder || 'Write something...'}
placeholder={placeholder || 'Write a comment'}
maxRows={MAX_ROWS}
minRows={computedMinRows}
onChange={handleInputChange}

View File

@ -0,0 +1,21 @@
import styled from '@emotion/styled';
const StyledPropertyBoxContainer = styled.div`
align-self: stretch;
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: 4px;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
`;
interface PropertyBoxProps {
children: JSX.Element;
extraPadding?: boolean;
}
export function PropertyBox({ children }: PropertyBoxProps) {
return <StyledPropertyBoxContainer>{children}</StyledPropertyBoxContainer>;
}

View File

@ -0,0 +1,56 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
const StyledPropertyBoxItem = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledIconContainer = styled.div`
align-items: center;
display: flex;
svg {
align-items: center;
display: flex;
height: 16px;
justify-content: center;
width: 16px;
}
`;
const StyledValueContainer = styled.div`
align-content: flex-start;
align-items: center;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex: 1 0 0;
flex-wrap: wrap;
`;
const StyledLabelAndIconContainer = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
export function PropertyBoxItem({
icon,
label,
value,
}: {
icon: ReactNode;
label?: string;
value: ReactNode;
}) {
return (
<StyledPropertyBoxItem>
<StyledLabelAndIconContainer>
<StyledIconContainer>{icon}</StyledIconContainer>
{label}
</StyledLabelAndIconContainer>
<StyledValueContainer>{value}</StyledValueContainer>
</StyledPropertyBoxItem>
);
}

View File

@ -9,7 +9,7 @@ const StyledMainSectionTitle = styled.h2`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.xl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
line-height: 1.5;
line-height: ${({ theme }) => theme.text.lineHeight.lg};
margin: 0;
`;

View File

@ -10,7 +10,7 @@ const StyledTitle = styled.div`
align-items: center;
display: flex;
flex-direction: row;
font-weight: 500;
font-weight: ${({ theme }) => theme.font.weight.medium};
height: ${({ theme }) => theme.spacing(8)};
padding-left: ${({ theme }) => theme.spacing(2)};
`;

View File

@ -32,7 +32,7 @@ const StyledButton = styled.div<StyledButtonProps>`
`;
const StyledButtonLabel = styled.div`
font-weight: 500;
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: ${({ theme }) => theme.spacing(2)};
`;

View File

@ -1,4 +1,4 @@
import { IconComment } from '@/ui/icons/index';
import { IconNotes } from '@/ui/icons/index';
import { EntityTableActionBarButton } from './EntityTableActionBarButton';
@ -9,8 +9,8 @@ type OwnProps = {
export function TableActionBarButtonToggleComments({ onClick }: OwnProps) {
return (
<EntityTableActionBarButton
label="Comment"
icon={<IconComment size={16} />}
label="Notes"
icon={<IconNotes size={16} />}
onClick={onClick}
/>
);

View File

@ -179,7 +179,7 @@ function DropdownButton({
};
const dropdownRef = useRef(null);
useOutsideAlerter(dropdownRef, onOutsideClick);
useOutsideAlerter({ ref: dropdownRef, callback: onOutsideClick });
return (
<StyledDropdownButtonContainer>

View File

@ -43,7 +43,7 @@ const StyledCancelButton = styled.button`
border: none;
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
font-weight: 500;
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: auto;
margin-right: ${({ theme }) => theme.spacing(2)};
padding: ${(props) => {

View File

@ -45,7 +45,7 @@ const StyledDelete = styled.div`
`;
const StyledLabelKey = styled.div`
font-weight: 500;
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
function SortOrFilterChip({

View File

@ -27,7 +27,7 @@ const StyledTableHeader = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
flex-direction: row;
font-weight: 500;
font-weight: ${({ theme }) => theme.font.weight.medium};
height: 40px;
justify-content: space-between;
padding-left: ${({ theme }) => theme.spacing(3)};
@ -49,7 +49,7 @@ const StyledViewSection = styled.div`
const StyledFilters = styled.div`
display: flex;
font-weight: 400;
font-weight: ${({ theme }) => theme.font.weight.regular};
gap: 2px;
`;