From 49a99c8ae6f8a3e4b49858a5b8a99a7e16b12991 Mon Sep 17 00:00:00 2001 From: Sammy Teillet Date: Thu, 8 Jun 2023 17:40:25 +0200 Subject: [PATCH] Sammy/t 392 aau i can drag and drop opportunities (#257) * refactor: extract data from Board component * feature: display board on opportunities page * test: add strict mode in storybook * chore: replace dnd to make it work with React 18 and strict mode Atlassion has not fixed this issue in a year so we use the fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350 * refactor: move mocked-data in a file * chore: use real column names in mock data * feature: design columns * feature: add New button at bottum of columns * bugfix: move header out of dragable so the cards does not flicker on drop * lint: remove useless imports * refactor: rename board item key --- front/package-lock.json | 120 ++++++++++-------- front/package.json | 3 +- .../src/modules/ui/components/board/Board.tsx | 50 +++----- .../modules/ui/components/board/BoardCard.tsx | 10 +- .../ui/components/board/BoardColumn.tsx | 42 ++++-- .../ui/components/board/BoardNewButton.tsx | 29 +++++ .../board/__stories__/Board.stories.tsx | 9 +- .../components/board/__stories__/mock-data.ts | 55 ++++++++ front/src/modules/ui/layout/styles/themes.ts | 3 + .../src/pages/opportunities/Opportunities.tsx | 8 +- 10 files changed, 224 insertions(+), 105 deletions(-) create mode 100644 front/src/modules/ui/components/board/BoardNewButton.tsx create mode 100644 front/src/modules/ui/components/board/__stories__/mock-data.ts diff --git a/front/package-lock.json b/front/package-lock.json index 075ea066a..bc815dd85 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -11,6 +11,7 @@ "@apollo/client": "^3.7.5", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.5", + "@hello-pangea/dnd": "^16.2.0", "@tabler/icons-react": "^2.20.0", "@tanstack/react-table": "^8.8.5", "@types/node": "^16.18.4", @@ -23,7 +24,6 @@ "libphonenumber-js": "^1.10.26", "luxon": "^3.3.0", "react": "^18.2.0", - "react-beautiful-dnd": "^13.1.1", "react-datepicker": "^4.11.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "^4.4.0", @@ -59,7 +59,6 @@ "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", "@types/luxon": "^3.3.0", - "@types/react-beautiful-dnd": "^13.1.4", "@types/react-datepicker": "^4.11.2", "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.45.0", @@ -4470,6 +4469,24 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@hello-pangea/dnd": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-16.2.0.tgz", + "integrity": "sha512-inACvMcvvLr34CG0P6+G/3bprVKhwswxjcsFUSJ+fpOGjhvDj9caiA9X3clby0lgJ6/ILIJjyedHZYECB7GAgA==", + "dependencies": { + "@babel/runtime": "^7.19.4", + "css-box-model": "^1.2.1", + "memoize-one": "^6.0.0", + "raf-schd": "^4.0.3", + "react-redux": "^8.0.4", + "redux": "^4.2.0", + "use-memo-one": "^1.1.3" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -11185,15 +11202,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-beautiful-dnd": { - "version": "13.1.4", - "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.4.tgz", - "integrity": "sha512-4bIBdzOr0aavN+88q3C7Pgz+xkb7tz3whORYrmSj77wfVEMfiWiooIwVWFR7KM2e+uGTe5BVrXqSfb0aHeflJA==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-datepicker": { "version": "4.11.2", "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.11.2.tgz", @@ -11214,17 +11222,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-redux": { - "version": "7.1.25", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz", - "integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -11340,6 +11337,11 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "dev": true }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@types/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", @@ -28473,9 +28475,9 @@ } }, "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" }, "node_modules/memoizerific": { "version": "1.11.3", @@ -32214,24 +32216,6 @@ "asap": "~2.0.6" } }, - "node_modules/react-beautiful-dnd": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", - "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", - "dependencies": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-colorful": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", @@ -32627,33 +32611,51 @@ } }, "node_modules/react-redux": { - "version": "7.2.9", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", - "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.7.tgz", + "integrity": "sha512-1vRQuCQI5Y2uNmrMXg81RXKiBHY3jBzvCvNmZF437O/Z9/pZ+ba2uYHbemYXb3g8rjsacBGo+/wmfrQKzMhJsg==", "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" }, "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" + "@reduxjs/toolkit": "^1 || ^2.0.0-beta.0", + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" }, "peerDependenciesMeta": { + "@reduxjs/toolkit": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, "react-dom": { "optional": true }, "react-native": { "optional": true + }, + "redux": { + "optional": true } } }, "node_modules/react-redux/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react-refresh": { "version": "0.14.0", @@ -37690,6 +37692,14 @@ "react-dom": "16.8.0 - 18" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", diff --git a/front/package.json b/front/package.json index 4ff280a3f..0665dbac4 100644 --- a/front/package.json +++ b/front/package.json @@ -6,6 +6,7 @@ "@apollo/client": "^3.7.5", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.5", + "@hello-pangea/dnd": "^16.2.0", "@tabler/icons-react": "^2.20.0", "@tanstack/react-table": "^8.8.5", "@types/node": "^16.18.4", @@ -18,7 +19,6 @@ "libphonenumber-js": "^1.10.26", "luxon": "^3.3.0", "react": "^18.2.0", - "react-beautiful-dnd": "^13.1.1", "react-datepicker": "^4.11.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "^4.4.0", @@ -103,7 +103,6 @@ "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.5.2", "@types/luxon": "^3.3.0", - "@types/react-beautiful-dnd": "^13.1.4", "@types/react-datepicker": "^4.11.2", "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.45.0", diff --git a/front/src/modules/ui/components/board/Board.tsx b/front/src/modules/ui/components/board/Board.tsx index 9d6edabaf..d1ce8d9fb 100644 --- a/front/src/modules/ui/components/board/Board.tsx +++ b/front/src/modules/ui/components/board/Board.tsx @@ -1,12 +1,14 @@ import { useCallback, useState } from 'react'; +import styled from '@emotion/styled'; import { DragDropContext, Draggable, Droppable, OnDragEndResponder, -} from 'react-beautiful-dnd'; -import styled from '@emotion/styled'; +} from '@hello-pangea/dnd'; +// Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd +// https://github.com/atlassian/react-beautiful-dnd/issues/2350 import { BoardCard } from './BoardCard'; import { BoardColumn } from './BoardColumn'; @@ -16,43 +18,27 @@ const StyledBoard = styled.div` height: 100%; `; -type ItemKey = `item-${number}`; -interface Item { +export type BoardItemKey = `item-${number}`; +export interface Item { id: string; content: string; } -interface Items { +export interface Items { [key: string]: Item; } -interface Column { +export interface Column { id: string; title: string; - itemKeys: ItemKey[]; + colorCode?: string; + itemKeys: BoardItemKey[]; } -const items: Items = { - 'item-1': { id: 'item-1', content: 'Item 1' }, - 'item-2': { id: 'item-2', content: 'Item 2' }, - 'item-3': { id: 'item-3', content: 'Item 3' }, - 'item-4': { id: 'item-4', content: 'Item 4' }, - 'item-5': { id: 'item-5', content: 'Item 5' }, - 'item-6': { id: 'item-6', content: 'Item 6' }, -} satisfies Record; +type BoardProps = { + initialBoard: Column[]; + items: Items; +}; -const initialBoard = [ - { - id: 'column-1', - title: 'Column 1', - itemKeys: ['item-1', 'item-2', 'item-3', 'item-4'], - }, - { - id: 'column-2', - title: 'Column 2', - itemKeys: ['item-5', 'item-6'], - }, -] satisfies Column[]; - -export const Board = () => { +export const Board = ({ initialBoard, items }: BoardProps) => { const [board, setBoard] = useState(initialBoard); const onDragEnd: OnDragEndResponder = useCallback( @@ -99,7 +85,11 @@ export const Board = () => { {(provided) => provided && ( - + {column.itemKeys.map((itemKey, index) => ( theme.secondaryBackground}; + border: 1px solid ${({ theme }) => theme.quaternaryBackground}; border-radius: 4px; - padding: 16px; - margin-bottom: 16px; + padding: 8px; + margin-bottom: 8px; + box-shadow: ${({ theme }) => theme.boxShadow}; `; type BoardCardProps = { diff --git a/front/src/modules/ui/components/board/BoardColumn.tsx b/front/src/modules/ui/components/board/BoardColumn.tsx index fc9560c6d..a1525edd2 100644 --- a/front/src/modules/ui/components/board/BoardColumn.tsx +++ b/front/src/modules/ui/components/board/BoardColumn.tsx @@ -1,35 +1,53 @@ -import { DroppableProvided } from 'react-beautiful-dnd'; import styled from '@emotion/styled'; +import { DroppableProvided } from '@hello-pangea/dnd'; + +import { NewButton } from './BoardNewButton'; const StyledColumn = styled.div` display: flex; flex-direction: column; width: 300px; - margin-right: 16px; - background-color: #f5f5f5; - border-radius: 4px; - padding: 16px; + background-color: ${({ theme }) => theme.primaryBackground}; + padding: ${({ theme }) => theme.spacing(2)}; `; +const StyledColumnTitle = styled.h3` + font-family: 'Inter'; + font-style: normal; + font-weight: ${({ theme }) => theme.fontWeightBold}; + font-size: ${({ theme }) => theme.fontSizeMedium}; + line-height: ${({ theme }) => theme.lineHeight}; + color: ${({ color }) => color}; + margin: 0; + margin-bottom: ${({ theme }) => theme.spacing(2)}; +`; + +const ItemContainer = styled.div``; + type BoardColumnProps = { title: string; + colorCode?: string; children: any[]; droppableProvided: DroppableProvided; }; export const BoardColumn = ({ title, + colorCode, children, droppableProvided, }: BoardColumnProps) => { return ( - -

{title}

- {children} - {droppableProvided.placeholder} + + • {title} + + {children} + {droppableProvided.placeholder} + + ); }; diff --git a/front/src/modules/ui/components/board/BoardNewButton.tsx b/front/src/modules/ui/components/board/BoardNewButton.tsx new file mode 100644 index 000000000..756101d17 --- /dev/null +++ b/front/src/modules/ui/components/board/BoardNewButton.tsx @@ -0,0 +1,29 @@ +import styled from '@emotion/styled'; +import { IconPlus } from '@tabler/icons-react'; + +const StyledButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + background-color: ${({ theme }) => theme.primaryBackground}; + color: ${({ theme }) => theme.text40}; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s ease-in-out; + align-self: baseline; + gap: ${({ theme }) => theme.spacing(1)}; + + &:hover { + background-color: ${({ theme }) => theme.secondaryBackground}; + } +`; + +export const NewButton = () => { + return ( + + + New + + ); +}; diff --git a/front/src/modules/ui/components/board/__stories__/Board.stories.tsx b/front/src/modules/ui/components/board/__stories__/Board.stories.tsx index 06c5d2323..8f951bcfb 100644 --- a/front/src/modules/ui/components/board/__stories__/Board.stories.tsx +++ b/front/src/modules/ui/components/board/__stories__/Board.stories.tsx @@ -1,7 +1,10 @@ +import { StrictMode } from 'react'; import { Meta, StoryObj } from '@storybook/react'; import { Board } from '../Board'; +import { initialBoard, items } from './mock-data'; + const meta: Meta = { title: 'Components/Board', component: Board, @@ -11,5 +14,9 @@ export default meta; type Story = StoryObj; export const OneColumnBoard: Story = { - render: () => , + render: () => ( + + + + ), }; diff --git a/front/src/modules/ui/components/board/__stories__/mock-data.ts b/front/src/modules/ui/components/board/__stories__/mock-data.ts new file mode 100644 index 000000000..769477a26 --- /dev/null +++ b/front/src/modules/ui/components/board/__stories__/mock-data.ts @@ -0,0 +1,55 @@ +import { Column, Items } from '../Board'; + +export const items: Items = { + 'item-1': { id: 'item-1', content: 'Item 1' }, + 'item-2': { id: 'item-2', content: 'Item 2' }, + 'item-3': { id: 'item-3', content: 'Item 3' }, + 'item-4': { id: 'item-4', content: 'Item 4' }, + 'item-5': { id: 'item-5', content: 'Item 5' }, + 'item-6': { id: 'item-6', content: 'Item 6' }, +}; +for (let i = 7; i <= 20; i++) { + const key = `item-${i}`; + items[key] = { id: key, content: `Item ${i}` }; +} + +export const initialBoard = [ + { + id: 'column-1', + title: 'New', + colorCode: '#B76796', + itemKeys: [ + 'item-1', + 'item-2', + 'item-3', + 'item-4', + 'item-7', + 'item-8', + 'item-9', + ], + }, + { + id: 'column-2', + title: 'Screening', + colorCode: '#CB912F', + itemKeys: ['item-5', 'item-6'], + }, + { + id: 'column-3', + colorCode: '#9065B0', + title: 'Meeting', + itemKeys: [], + }, + { + id: 'column-4', + title: 'Proposal', + colorCode: '#337EA9', + itemKeys: [], + }, + { + id: 'column-5', + colorCode: '#079039', + title: 'Customer', + itemKeys: [], + }, +] satisfies Column[]; diff --git a/front/src/modules/ui/layout/styles/themes.ts b/front/src/modules/ui/layout/styles/themes.ts index 95f365cb3..20e730fe8 100644 --- a/front/src/modules/ui/layout/styles/themes.ts +++ b/front/src/modules/ui/layout/styles/themes.ts @@ -67,6 +67,8 @@ const lightThemeSpecific = { blueHighTransparency: 'rgba(25, 97, 237, 0.03)', blueLowTransparency: 'rgba(25, 97, 237, 0.32)', + + boxShadow: '0px 2px 4px 0px #0F0F0F0A', }; const darkThemeSpecific: typeof lightThemeSpecific = { @@ -107,6 +109,7 @@ const darkThemeSpecific: typeof lightThemeSpecific = { blueHighTransparency: 'rgba(104, 149, 236, 0.03)', blueLowTransparency: 'rgba(104, 149, 236, 0.32)', + boxShadow: '0px 2px 4px 0px #0F0F0F0A', // TODO change color for dark theme }; export const overlayBackground = (props: any) => diff --git a/front/src/pages/opportunities/Opportunities.tsx b/front/src/pages/opportunities/Opportunities.tsx index ce3e7f41f..e026e2294 100644 --- a/front/src/pages/opportunities/Opportunities.tsx +++ b/front/src/pages/opportunities/Opportunities.tsx @@ -2,10 +2,16 @@ import { FaBullseye } from 'react-icons/fa'; import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer'; +import { + initialBoard, + items, +} from '../../modules/ui/components/board/__stories__/mock-data'; +import { Board } from '../../modules/ui/components/board/Board'; + export function Opportunities() { return ( }> - <> + ); }