mirror of
				https://github.com/lingble/twenty.git
				synced 2025-11-03 22:27:57 +00:00 
			
		
		
		
	Add dropdown on Sort button on table
This commit is contained in:
		@@ -36,6 +36,7 @@ cd infra/dev
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
make build
 | 
					make build
 | 
				
			||||||
 | 
					make up
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Once this is completed you should have:
 | 
					Once this is completed you should have:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,3 @@
 | 
				
			|||||||
import { MemoryRouter } from 'react-router-dom';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import Checkbox from '../Checkbox';
 | 
					import Checkbox from '../Checkbox';
 | 
				
			||||||
import { ThemeProvider } from '@emotion/react';
 | 
					import { ThemeProvider } from '@emotion/react';
 | 
				
			||||||
import { lightTheme } from '../../../layout/styles/themes';
 | 
					import { lightTheme } from '../../../layout/styles/themes';
 | 
				
			||||||
@@ -12,9 +10,7 @@ export default {
 | 
				
			|||||||
export const RegularCheckbox = () => {
 | 
					export const RegularCheckbox = () => {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <ThemeProvider theme={lightTheme}>
 | 
					    <ThemeProvider theme={lightTheme}>
 | 
				
			||||||
      <MemoryRouter initialEntries={['/companies']}>
 | 
					 | 
				
			||||||
      <Checkbox name="selected-company-1" id="selected-company--1" />
 | 
					      <Checkbox name="selected-company-1" id="selected-company--1" />
 | 
				
			||||||
      </MemoryRouter>
 | 
					 | 
				
			||||||
    </ThemeProvider>
 | 
					    </ThemeProvider>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ import { render } from '@testing-library/react';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { RegularCheckbox } from '../__stories__/Checkbox.stories';
 | 
					import { RegularCheckbox } from '../__stories__/Checkbox.stories';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
it('Checks the NavItem renders', () => {
 | 
					it('Checks the Checkbox renders', () => {
 | 
				
			||||||
  const { getByTestId } = render(<RegularCheckbox />);
 | 
					  const { getByTestId } = render(<RegularCheckbox />);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  expect(getByTestId('input-checkbox')).toHaveAttribute(
 | 
					  expect(getByTestId('input-checkbox')).toHaveAttribute(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ import {
 | 
				
			|||||||
  getCoreRowModel,
 | 
					  getCoreRowModel,
 | 
				
			||||||
  useReactTable,
 | 
					  useReactTable,
 | 
				
			||||||
} from '@tanstack/react-table';
 | 
					} from '@tanstack/react-table';
 | 
				
			||||||
import TableHeader from './TableHeader';
 | 
					import TableHeader from './table-header/TableHeader';
 | 
				
			||||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
 | 
					import { IconProp } from '@fortawesome/fontawesome-svg-core';
 | 
				
			||||||
import styled from '@emotion/styled';
 | 
					import styled from '@emotion/styled';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										106
									
								
								front/src/components/table/table-header/DropdownButton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								front/src/components/table/table-header/DropdownButton.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					import styled from '@emotion/styled';
 | 
				
			||||||
 | 
					import { IconProp } from '@fortawesome/fontawesome-svg-core';
 | 
				
			||||||
 | 
					import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 | 
				
			||||||
 | 
					import { useState, useRef } from 'react';
 | 
				
			||||||
 | 
					import { useOutsideAlerter } from '../../../hooks/useOutsideAlerter';
 | 
				
			||||||
 | 
					import { modalBackground } from '../../../layout/styles/themes';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type OwnProps = {
 | 
				
			||||||
 | 
					  label: string;
 | 
				
			||||||
 | 
					  options: Array<{ label: string; icon: IconProp }>;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledDropdownButtonContainer = styled.div`
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type StyledDropdownButtonProps = {
 | 
				
			||||||
 | 
					  isUnfolded: boolean;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledDropdownButton = styled.div<StyledDropdownButtonProps>`
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  margin-left: ${(props) => props.theme.spacing(3)};
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  background: ${(props) => props.theme.primaryBackground};
 | 
				
			||||||
 | 
					  padding: ${(props) => props.theme.spacing(1)};
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  filter: ${(props) => (props.isUnfolded ? 'brightness(0.95)' : 'none')};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    filter: brightness(0.95);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledDropdown = styled.ul`
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 14px;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  border: 1px solid ${(props) => props.theme.primaryBorder};
 | 
				
			||||||
 | 
					  box-shadow: 0px 3px 12px rgba(0, 0, 0, 0.09);
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  padding: 0px;
 | 
				
			||||||
 | 
					  min-width: 160px;
 | 
				
			||||||
 | 
					  ${modalBackground}
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledDropdownItem = styled.li`
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  padding: ${(props) => props.theme.spacing(2)}
 | 
				
			||||||
 | 
					    calc(${(props) => props.theme.spacing(2)} - 2px);
 | 
				
			||||||
 | 
					  margin: 2px;
 | 
				
			||||||
 | 
					  background: ${(props) => props.theme.primaryBackground};
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  color: ${(props) => props.theme.text60};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:hover {
 | 
				
			||||||
 | 
					    filter: brightness(0.95);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledIcon = styled.div`
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  margin-right: ${(props) => props.theme.spacing(1)};
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function DropdownButton({ label, options }: OwnProps) {
 | 
				
			||||||
 | 
					  const [isUnfolded, setIsUnfolded] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onButtonClick = () => {
 | 
				
			||||||
 | 
					    setIsUnfolded(!isUnfolded);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onOutsideClick = () => {
 | 
				
			||||||
 | 
					    setIsUnfolded(false);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const dropdownRef = useRef(null);
 | 
				
			||||||
 | 
					  useOutsideAlerter(dropdownRef, onOutsideClick);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <StyledDropdownButtonContainer>
 | 
				
			||||||
 | 
					      <StyledDropdownButton isUnfolded={isUnfolded} onClick={onButtonClick}>
 | 
				
			||||||
 | 
					        {label}
 | 
				
			||||||
 | 
					      </StyledDropdownButton>
 | 
				
			||||||
 | 
					      {isUnfolded && options.length > 0 && (
 | 
				
			||||||
 | 
					        <StyledDropdown ref={dropdownRef}>
 | 
				
			||||||
 | 
					          {options.map((option, index) => (
 | 
				
			||||||
 | 
					            <StyledDropdownItem key={index}>
 | 
				
			||||||
 | 
					              <StyledIcon>
 | 
				
			||||||
 | 
					                <FontAwesomeIcon icon={option.icon} />
 | 
				
			||||||
 | 
					              </StyledIcon>
 | 
				
			||||||
 | 
					              {option.label}
 | 
				
			||||||
 | 
					            </StyledDropdownItem>
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					        </StyledDropdown>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </StyledDropdownButtonContainer>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default DropdownButton;
 | 
				
			||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
import styled from '@emotion/styled';
 | 
					import styled from '@emotion/styled';
 | 
				
			||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 | 
					import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 | 
				
			||||||
 | 
					import DropdownButton from './DropdownButton';
 | 
				
			||||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
 | 
					import { IconProp } from '@fortawesome/fontawesome-svg-core';
 | 
				
			||||||
 | 
					import { faCalendar } from '@fortawesome/pro-regular-svg-icons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type OwnProps = {
 | 
					type OwnProps = {
 | 
				
			||||||
  viewName: string;
 | 
					  viewName: string;
 | 
				
			||||||
@@ -30,12 +32,7 @@ const StyledViewSection = styled.div`
 | 
				
			|||||||
const StyledFilters = styled.div`
 | 
					const StyledFilters = styled.div`
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  font-weight: 400;
 | 
					  font-weight: 400;
 | 
				
			||||||
  margin-right: ${(props) => props.theme.spacing(1)};
 | 
					  margin-right: ${(props) => props.theme.spacing(2)};
 | 
				
			||||||
`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const StyledFilterButton = styled.div`
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  margin-left: ${(props) => props.theme.spacing(4)};
 | 
					 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function TableHeader({ viewName, viewIcon }: OwnProps) {
 | 
					function TableHeader({ viewName, viewIcon }: OwnProps) {
 | 
				
			||||||
@@ -48,9 +45,12 @@ function TableHeader({ viewName, viewIcon }: OwnProps) {
 | 
				
			|||||||
        {viewName}
 | 
					        {viewName}
 | 
				
			||||||
      </StyledViewSection>
 | 
					      </StyledViewSection>
 | 
				
			||||||
      <StyledFilters>
 | 
					      <StyledFilters>
 | 
				
			||||||
        <StyledFilterButton>Filter</StyledFilterButton>
 | 
					        <DropdownButton label="Filter" options={[]} />
 | 
				
			||||||
        <StyledFilterButton>Sort</StyledFilterButton>
 | 
					        <DropdownButton
 | 
				
			||||||
        <StyledFilterButton>Settings</StyledFilterButton>
 | 
					          label="Sort"
 | 
				
			||||||
 | 
					          options={[{ label: 'Created at', icon: faCalendar }]}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <DropdownButton label="Settings" options={[]} />
 | 
				
			||||||
      </StyledFilters>
 | 
					      </StyledFilters>
 | 
				
			||||||
    </StyledTitle>
 | 
					    </StyledTitle>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import TableHeader from '../TableHeader';
 | 
				
			||||||
 | 
					import { ThemeProvider } from '@emotion/react';
 | 
				
			||||||
 | 
					import { lightTheme } from '../../../../layout/styles/themes';
 | 
				
			||||||
 | 
					import { faBuilding } from '@fortawesome/pro-regular-svg-icons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  title: 'TableHeader',
 | 
				
			||||||
 | 
					  component: TableHeader,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const RegularTableHeader = () => {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ThemeProvider theme={lightTheme}>
 | 
				
			||||||
 | 
					      <TableHeader viewName="Test" viewIcon={faBuilding} />
 | 
				
			||||||
 | 
					    </ThemeProvider>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					import { render } from '@testing-library/react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { RegularTableHeader } from '../__stories__/TableHeader.stories';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					it('Checks the TableHeader renders', () => {
 | 
				
			||||||
 | 
					  const { getByText } = render(<RegularTableHeader />);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  expect(getByText('Test')).toBeDefined();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										33
									
								
								front/src/hooks/__tests__/useOutsideAlerter.test.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								front/src/hooks/__tests__/useOutsideAlerter.test.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					const onOutsideClick = jest.fn();
 | 
				
			||||||
 | 
					import { useRef } from 'react';
 | 
				
			||||||
 | 
					import TableHeader from '../../components/table/table-header/TableHeader';
 | 
				
			||||||
 | 
					import { render, fireEvent } from '@testing-library/react';
 | 
				
			||||||
 | 
					import { useOutsideAlerter } from '../useOutsideAlerter';
 | 
				
			||||||
 | 
					import { act } from 'react-dom/test-utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function TestComponent() {
 | 
				
			||||||
 | 
					  const buttonRef = useRef(null);
 | 
				
			||||||
 | 
					  useOutsideAlerter(buttonRef, onOutsideClick);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      <span>Outside</span>
 | 
				
			||||||
 | 
					      <button ref={buttonRef}>Inside</button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default TableHeader;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('clicking the button toggles an answer on/off', async () => {
 | 
				
			||||||
 | 
					  const { getByText } = render(<TestComponent />);
 | 
				
			||||||
 | 
					  const inside = getByText('Inside');
 | 
				
			||||||
 | 
					  const outside = getByText('Outside');
 | 
				
			||||||
 | 
					  await act(() => Promise.resolve());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fireEvent.mouseDown(inside);
 | 
				
			||||||
 | 
					  expect(onOutsideClick).toHaveBeenCalledTimes(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fireEvent.mouseDown(outside);
 | 
				
			||||||
 | 
					  expect(onOutsideClick).toHaveBeenCalledTimes(1);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										23
									
								
								front/src/hooks/useOutsideAlerter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								front/src/hooks/useOutsideAlerter.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					import { useEffect } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare type CallbackType = () => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useOutsideAlerter(
 | 
				
			||||||
 | 
					  ref: React.RefObject<HTMLInputElement>,
 | 
				
			||||||
 | 
					  callback: CallbackType,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    function handleClickOutside(event: Event) {
 | 
				
			||||||
 | 
					      console.log('test3');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const target = event.target as HTMLButtonElement;
 | 
				
			||||||
 | 
					      if (ref.current && !ref.current.contains(target)) {
 | 
				
			||||||
 | 
					        callback();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    document.addEventListener('mousedown', handleClickOutside);
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      document.removeEventListener('mousedown', handleClickOutside);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [ref]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					import { css } from '@emotion/react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const commonTheme = {
 | 
					const commonTheme = {
 | 
				
			||||||
  fontSizeSmall: '0.92rem',
 | 
					  fontSizeSmall: '0.92rem',
 | 
				
			||||||
  fontSizeMedium: '1rem',
 | 
					  fontSizeMedium: '1rem',
 | 
				
			||||||
@@ -21,6 +23,8 @@ const lightThemeSpecific = {
 | 
				
			|||||||
  purpleBackground: '#e0e0ff',
 | 
					  purpleBackground: '#e0e0ff',
 | 
				
			||||||
  yellowBackground: '#fff2e7',
 | 
					  yellowBackground: '#fff2e7',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  secondaryBackgroundSmallTransparency: 'rgba(252, 252, 252, 0.8)',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  primaryBorder: 'rgba(0, 0, 0, 0.08)',
 | 
					  primaryBorder: 'rgba(0, 0, 0, 0.08)',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  text100: '#000',
 | 
					  text100: '#000',
 | 
				
			||||||
@@ -49,6 +53,10 @@ const darkThemeSpecific = {
 | 
				
			|||||||
  purpleBackground: '#1111b7',
 | 
					  purpleBackground: '#1111b7',
 | 
				
			||||||
  yellowBackground: '#cc660a',
 | 
					  yellowBackground: '#cc660a',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  secondaryBackgroundSmallTransparency: 'rgba(23, 23, 23, 0.8)',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  primaryBorder: 'rgba(255, 255, 255, 0.08)',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  text100: '#ffffff',
 | 
					  text100: '#ffffff',
 | 
				
			||||||
  text80: '#ccc',
 | 
					  text80: '#ccc',
 | 
				
			||||||
  text60: '#999',
 | 
					  text60: '#999',
 | 
				
			||||||
@@ -64,6 +72,12 @@ const darkThemeSpecific = {
 | 
				
			|||||||
  yellow: '#fff2e7',
 | 
					  yellow: '#fff2e7',
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const modalBackground = (props: any) =>
 | 
				
			||||||
 | 
					  css`
 | 
				
			||||||
 | 
					    backdrop-filter: blur(20px);
 | 
				
			||||||
 | 
					    background: ${props.theme.secondaryBackgroundSmallTransparency};
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const lightTheme = { ...commonTheme, ...lightThemeSpecific };
 | 
					export const lightTheme = { ...commonTheme, ...lightThemeSpecific };
 | 
				
			||||||
export const darkTheme = { ...commonTheme, ...darkThemeSpecific };
 | 
					export const darkTheme = { ...commonTheme, ...darkThemeSpecific };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user