From f647e1ff1107d3b6efef9936815ec76455fc36ef Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 9 May 2024 13:22:58 -0400 Subject: [PATCH] Introduce React-Trello in place of React-Kanban Signed-off-by: Dave Richer --- README.MD | 4 +- _reference/test api setup.md | 2 +- _reference/test-ecoystem.config.js | 2 +- client/cypress/plugins/index.js | 2 +- client/cypress/support/e2e.js | 2 +- client/package-lock.json | 99 +++++- client/package.json | 7 + client/public/3rdparty-api.txt | 2 +- client/public/3rdparty-app.txt | 2 +- .../jobs-list/jobs-list.component.jsx | 1 - ...production-board-kanban-card.component.jsx | 85 ++--- .../production-board-kanban.component.jsx | 73 ++-- .../production-board-kanban.utils.js | 37 +- ...-list-columns.productionnote.component.jsx | 3 +- .../production-sublets-manage.component.jsx | 2 + .../trello-board/components/AddCardLink.jsx | 11 + .../trello-board/components/Card.jsx | 112 ++++++ .../trello-board/components/Card/Tag.jsx | 21 ++ .../components/Lane/LaneFooter.jsx | 9 + .../components/Lane/LaneHeader.jsx | 64 ++++ .../components/Lane/LaneHeader/LaneMenu.jsx | 41 +++ .../trello-board/components/Loader.jsx | 13 + .../trello-board/components/NewCardForm.jsx | 53 +++ .../trello-board/components/NewLaneForm.jsx | 56 +++ .../components/NewLaneSection.jsx | 16 + .../trello-board/components/index.js | 24 ++ .../trello-board/controllers/Board.jsx | 19 + .../controllers/BoardContainer.jsx | 294 ++++++++++++++++ .../trello-board/controllers/Lane.jsx | 328 ++++++++++++++++++ .../components/trello-board/dnd/Container.jsx | 139 ++++++++ .../components/trello-board/dnd/Draggable.jsx | 26 ++ .../trello-board/helpers/LaneHelper.js | 135 +++++++ .../helpers/deprecationWarnings.js | 24 ++ client/src/components/trello-board/index.jsx | 38 ++ .../components/trello-board/styles/Base.js | 298 ++++++++++++++++ .../trello-board/styles/Elements.js | 251 ++++++++++++++ .../components/trello-board/styles/Loader.js | 43 +++ .../trello-board/widgets/DeleteButton.jsx | 12 + .../trello-board/widgets/EditableLabel.jsx | 87 +++++ .../trello-board/widgets/InlineInput.jsx | 106 ++++++ .../widgets/NewLaneTitleEditor.jsx | 94 +++++ .../components/trello-board/widgets/index.jsx | 9 + client/src/redux/root.reducer.js | 10 +- client/src/redux/trello/trello.actions.js | 14 + client/src/redux/trello/trello.reducer.js | 35 ++ client/src/translations/en_us/common.json | 14 +- client/src/translations/es/common.json | 14 +- client/src/translations/fr/common.json | 14 +- client/vite.config.js | 4 +- 49 files changed, 2632 insertions(+), 119 deletions(-) create mode 100644 client/src/components/trello-board/components/AddCardLink.jsx create mode 100644 client/src/components/trello-board/components/Card.jsx create mode 100644 client/src/components/trello-board/components/Card/Tag.jsx create mode 100644 client/src/components/trello-board/components/Lane/LaneFooter.jsx create mode 100644 client/src/components/trello-board/components/Lane/LaneHeader.jsx create mode 100644 client/src/components/trello-board/components/Lane/LaneHeader/LaneMenu.jsx create mode 100644 client/src/components/trello-board/components/Loader.jsx create mode 100644 client/src/components/trello-board/components/NewCardForm.jsx create mode 100644 client/src/components/trello-board/components/NewLaneForm.jsx create mode 100644 client/src/components/trello-board/components/NewLaneSection.jsx create mode 100644 client/src/components/trello-board/components/index.js create mode 100644 client/src/components/trello-board/controllers/Board.jsx create mode 100644 client/src/components/trello-board/controllers/BoardContainer.jsx create mode 100644 client/src/components/trello-board/controllers/Lane.jsx create mode 100644 client/src/components/trello-board/dnd/Container.jsx create mode 100644 client/src/components/trello-board/dnd/Draggable.jsx create mode 100644 client/src/components/trello-board/helpers/LaneHelper.js create mode 100644 client/src/components/trello-board/helpers/deprecationWarnings.js create mode 100644 client/src/components/trello-board/index.jsx create mode 100644 client/src/components/trello-board/styles/Base.js create mode 100644 client/src/components/trello-board/styles/Elements.js create mode 100644 client/src/components/trello-board/styles/Loader.js create mode 100644 client/src/components/trello-board/widgets/DeleteButton.jsx create mode 100644 client/src/components/trello-board/widgets/EditableLabel.jsx create mode 100644 client/src/components/trello-board/widgets/InlineInput.jsx create mode 100644 client/src/components/trello-board/widgets/NewLaneTitleEditor.jsx create mode 100644 client/src/components/trello-board/widgets/index.jsx create mode 100644 client/src/redux/trello/trello.actions.js create mode 100644 client/src/redux/trello/trello.reducer.js diff --git a/README.MD b/README.MD index dcffca3fc..e0b562dfa 100644 --- a/README.MD +++ b/README.MD @@ -2,7 +2,7 @@ NGROK TEsting: ./ngrok.exe http http://localhost:4000 -host-header="localhost:4000" Finding deadfiles - run from client directory -npx deadfile ./src/index.js --exclude build templates +npx deadfile ./src/index.jsx --exclude build templates #Crushing all hasura migrations by creating a new initialization from the server. hasura migrate create "Init" --from-server --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#' @@ -11,4 +11,4 @@ Production-ImEXOnline!@#' hasura migrate status --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#' Generate the license file: -$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite \ No newline at end of file +$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite diff --git a/_reference/test api setup.md b/_reference/test api setup.md index 791c5b571..813d4a3e9 100644 --- a/_reference/test api setup.md +++ b/_reference/test api setup.md @@ -4,7 +4,7 @@ Clone Repository for: { "name": "node-webhook-scripts", "version": "1.0.0", - "main": "index.js", + "main": "index.jsx", "dependencies": { "express": "^4.16.4" }, diff --git a/_reference/test-ecoystem.config.js b/_reference/test-ecoystem.config.js index 1bf66e16c..fdf2abe6f 100644 --- a/_reference/test-ecoystem.config.js +++ b/_reference/test-ecoystem.config.js @@ -11,7 +11,7 @@ module.exports = { { name: "Bitbucket Webhook", - script: "./webhook/index.js", + script: "./webhook/index.jsx", env: { NODE_ENV: "production" } diff --git a/client/cypress/plugins/index.js b/client/cypress/plugins/index.js index 8229063ad..e03c48d6e 100644 --- a/client/cypress/plugins/index.js +++ b/client/cypress/plugins/index.js @@ -1,6 +1,6 @@ /// // *********************************************************** -// This example plugins/index.js can be used to load plugins +// This example plugins/index.jsx can be used to load plugins // // You can change the location of this file or turn off loading // the plugins file with the 'pluginsFile' configuration option. diff --git a/client/cypress/support/e2e.js b/client/cypress/support/e2e.js index d076cec9f..e328a179b 100644 --- a/client/cypress/support/e2e.js +++ b/client/cypress/support/e2e.js @@ -1,5 +1,5 @@ // *********************************************************** -// This example support/index.js is processed and +// This example support/index.jsx is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and diff --git a/client/package-lock.json b/client/package-lock.json index a4f887a06..3c1b53bf2 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,6 +12,7 @@ "@ant-design/pro-layout": "^7.17.16", "@apollo/client": "^3.8.10", "@asseinfo/react-kanban": "^2.2.0", + "@emotion/is-prop-valid": "^1.2.2", "@fingerprintjs/fingerprintjs": "^4.2.2", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.2.1", @@ -23,7 +24,9 @@ "antd": "^5.15.3", "apollo-link-logger": "^2.0.1", "apollo-link-sentry": "^3.3.0", + "autosize": "^6.0.1", "axios": "^1.6.7", + "classnames": "^2.5.1", "dayjs": "^1.11.10", "dayjs-business-days2": "^1.2.2", "dinero.js": "^1.9.1", @@ -34,6 +37,8 @@ "graphql": "^16.6.0", "i18next": "^23.10.0", "i18next-browser-languagedetector": "^7.0.2", + "immutability-helper": "^3.1.1", + "kuika-smooth-dnd": "^1.0.0", "libphonenumber-js": "^1.10.57", "logrocket": "^8.0.1", "markerjs2": "^2.32.0", @@ -54,6 +59,7 @@ "react-joyride": "^2.7.4", "react-markdown": "^9.0.1", "react-number-format": "^5.3.3", + "react-popopo": "^2.1.9", "react-product-fruits": "^2.2.6", "react-redux": "^9.1.0", "react-resizable": "^3.0.5", @@ -63,6 +69,7 @@ "react-virtualized": "^9.22.5", "recharts": "^2.12.2", "redux": "^5.0.1", + "redux-actions": "^2.6.5", "redux-persist": "^6.0.0", "redux-saga": "^1.3.0", "redux-state-sync": "^3.1.4", @@ -2660,8 +2667,9 @@ "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { - "version": "1.2.1", - "license": "MIT", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", "dependencies": { "@emotion/memoize": "^0.8.1" } @@ -8151,6 +8159,11 @@ "postcss": "^8.1.0" } }, + "node_modules/autosize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/autosize/-/autosize-6.0.1.tgz", + "integrity": "sha512-f86EjiUKE6Xvczc4ioP1JBlWG7FKrE13qe/DxBCpe8GCipCq2nFw73aO8QEBKHfSbYGDN5eB9jXWKen7tspDqQ==" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "license": "MIT", @@ -9396,7 +9409,8 @@ }, "node_modules/classnames": { "version": "2.5.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, "node_modules/clean-css": { "version": "5.3.3", @@ -14416,6 +14430,11 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/immutability-helper": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz", + "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==" + }, "node_modules/immutable": { "version": "4.3.5", "license": "MIT" @@ -17464,6 +17483,11 @@ "node": ">=4.0" } }, + "node_modules/just-curry-it": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/just-curry-it/-/just-curry-it-3.2.1.tgz", + "integrity": "sha512-Q8206k8pTY7krW32cdmPsP+DqqLgWx/hYPSj9/+7SYqSqz7UuwPbfSe07lQtvuuaVyiSJveXk0E5RydOuWwsEg==" + }, "node_modules/keyv": { "version": "4.5.4", "license": "MIT", @@ -17492,6 +17516,11 @@ "node": ">= 8" } }, + "node_modules/kuika-smooth-dnd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/kuika-smooth-dnd/-/kuika-smooth-dnd-1.0.0.tgz", + "integrity": "sha512-bNv7SBo9IB+ovMmBMYw9IS24f7B8Mek5uO+E4cGKhUjthIquxsIIszmOcdvbF+8t+2GAvTsqW6lsHSmd3Ry6/Q==" + }, "node_modules/kuler": { "version": "2.0.0", "dev": true, @@ -22472,6 +22501,24 @@ "react-dom": ">=16.3.0" } }, + "node_modules/react-popopo": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/react-popopo/-/react-popopo-2.1.9.tgz", + "integrity": "sha512-zXOpcLSpaLZmBxhdtenJzQPLjY81XknVS/tXH4Kv5BBrnYIUPHvVdGmS7+o9s7DjCzzdK7AdVwtG+FVSO0cZ8g==", + "dependencies": { + "classnames": ">= 2.0", + "prop-types": "^15.7.2", + "react": ">= 16.3", + "react-dom": ">= 16.3", + "styled-components": ">= 4.0" + }, + "peerDependencies": { + "classnames": ">= 2.0", + "react": ">= 16.3", + "react-dom": ">= 16.3", + "styled-components": ">= 4.0" + } + }, "node_modules/react-product-fruits": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/react-product-fruits/-/react-product-fruits-2.2.6.tgz", @@ -22860,10 +22907,27 @@ "node": ">=4" } }, + "node_modules/reduce-reducers": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/reduce-reducers/-/reduce-reducers-0.4.3.tgz", + "integrity": "sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw==" + }, "node_modules/redux": { "version": "5.0.1", "license": "MIT" }, + "node_modules/redux-actions": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/redux-actions/-/redux-actions-2.6.5.tgz", + "integrity": "sha512-pFhEcWFTYNk7DhQgxMGnbsB1H2glqhQJRQrtPb96kD3hWiZRzXHwwmFPswg6V2MjraXRXWNmuP9P84tvdLAJmw==", + "dependencies": { + "invariant": "^2.2.4", + "just-curry-it": "^3.1.0", + "loose-envify": "^1.4.0", + "reduce-reducers": "^0.4.3", + "to-camel-case": "^1.0.0" + } + }, "node_modules/redux-logger": { "version": "3.0.6", "dev": true, @@ -24779,6 +24843,14 @@ "react-dom": ">= 16.8.0" } }, + "node_modules/styled-components/node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, "node_modules/styled-components/node_modules/@emotion/unitless": { "version": "0.8.0", "license": "MIT" @@ -25416,6 +25488,14 @@ "version": "1.0.5", "license": "BSD-3-Clause" }, + "node_modules/to-camel-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz", + "integrity": "sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==", + "dependencies": { + "to-space-case": "^1.0.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "license": "MIT", @@ -25423,6 +25503,11 @@ "node": ">=4" } }, + "node_modules/to-no-case": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", + "integrity": "sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==" + }, "node_modules/to-readable-stream": { "version": "1.0.0", "dev": true, @@ -25441,6 +25526,14 @@ "node": ">=8.0" } }, + "node_modules/to-space-case": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz", + "integrity": "sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==", + "dependencies": { + "to-no-case": "^1.0.0" + } + }, "node_modules/toggle-selection": { "version": "1.0.6", "license": "MIT" diff --git a/client/package.json b/client/package.json index 9af0befa4..1e9dd07aa 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ "@ant-design/pro-layout": "^7.17.16", "@apollo/client": "^3.8.10", "@asseinfo/react-kanban": "^2.2.0", + "@emotion/is-prop-valid": "^1.2.2", "@fingerprintjs/fingerprintjs": "^4.2.2", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.2.1", @@ -23,7 +24,9 @@ "antd": "^5.15.3", "apollo-link-logger": "^2.0.1", "apollo-link-sentry": "^3.3.0", + "autosize": "^6.0.1", "axios": "^1.6.7", + "classnames": "^2.5.1", "dayjs": "^1.11.10", "dayjs-business-days2": "^1.2.2", "dinero.js": "^1.9.1", @@ -34,6 +37,8 @@ "graphql": "^16.6.0", "i18next": "^23.10.0", "i18next-browser-languagedetector": "^7.0.2", + "immutability-helper": "^3.1.1", + "kuika-smooth-dnd": "^1.0.0", "libphonenumber-js": "^1.10.57", "logrocket": "^8.0.1", "markerjs2": "^2.32.0", @@ -54,6 +59,7 @@ "react-joyride": "^2.7.4", "react-markdown": "^9.0.1", "react-number-format": "^5.3.3", + "react-popopo": "^2.1.9", "react-product-fruits": "^2.2.6", "react-redux": "^9.1.0", "react-resizable": "^3.0.5", @@ -63,6 +69,7 @@ "react-virtualized": "^9.22.5", "recharts": "^2.12.2", "redux": "^5.0.1", + "redux-actions": "^2.6.5", "redux-persist": "^6.0.0", "redux-saga": "^1.3.0", "redux-state-sync": "^3.1.4", diff --git a/client/public/3rdparty-api.txt b/client/public/3rdparty-api.txt index f5acd9569..69a07ab06 100644 --- a/client/public/3rdparty-api.txt +++ b/client/public/3rdparty-api.txt @@ -16422,7 +16422,7 @@ For when you don't want to write the same thing over and over to cache a method $ npm install --save-dev stubs ``` ```js -var mylib = require('./lib/index.js') +var mylib = require('./lib/index.jsx') var stubs = require('stubs') // make it a noop diff --git a/client/public/3rdparty-app.txt b/client/public/3rdparty-app.txt index 8fe9b97b2..20022a514 100644 --- a/client/public/3rdparty-app.txt +++ b/client/public/3rdparty-app.txt @@ -16567,7 +16567,7 @@ even more slower. ## Benchmarks ```bash -$ node benchmarks/index.js +$ node benchmarks/index.jsx Benchmarking: sign elliptic#sign x 262 ops/sec ±0.51% (177 runs sampled) eccjs#sign x 55.91 ops/sec ±0.90% (144 runs sampled) diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index 81273a7e5..6082055d7 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -18,7 +18,6 @@ import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import { setJoyRideSteps } from "../../redux/application/application.actions"; import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component"; -import InstanceRenderManager from "../../utils/instanceRenderMgr"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop diff --git a/client/src/components/production-board-kanban-card/production-board-kanban-card.component.jsx b/client/src/components/production-board-kanban-card/production-board-kanban-card.component.jsx index fcb0b8fce..bfe075b2b 100644 --- a/client/src/components/production-board-kanban-card/production-board-kanban-card.component.jsx +++ b/client/src/components/production-board-kanban-card/production-board-kanban-card.component.jsx @@ -40,55 +40,60 @@ function getContrastYIQ(bgColor) { return yiq >= 128 ? "black" : "white"; } -export default function ProductionBoardCard(technician, card, bodyshop, cardSettings) { +export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings }) { const { t } = useTranslation(); let employee_body, employee_prep, employee_refinish, employee_csr; - if (card.employee_body) { - employee_body = bodyshop.employees.find((e) => e.id === card.employee_body); + + if (card && card.metadata && card.metadata.employee_body) { + employee_body = bodyshop.employees.find((e) => e.id === card.metadata.employee_body); } - if (card.employee_prep) { - employee_prep = bodyshop.employees.find((e) => e.id === card.employee_prep); + if (card && card.metadata && card.metadata.employee_prep) { + employee_prep = bodyshop.employees.find((e) => e.id === card.metadata.employee_prep); } - if (card.employee_refinish) { - employee_refinish = bodyshop.employees.find((e) => e.id === card.employee_refinish); + if (card && card.metadata && card.metadata.employee_refinish) { + employee_refinish = bodyshop.employees.find((e) => e.id === card.metadata.employee_refinish); } - if (card.employee_csr) { - employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr); + if (card && card.metadata && card.metadata.employee_csr) { + employee_csr = bodyshop.employees.find((e) => e.id === card.metadata.employee_csr); } - // if (card.employee_csr) { - // employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr); + // if (card && card.metadata && card.metadata.employee_csr) { + // employee_csr = bodyshop.employees.find((e) => e.id === card.metadata.employee_csr); // } const pastDueAlert = - !!card.scheduled_completion && - ((dayjs().isSameOrAfter(dayjs(card.scheduled_completion), "day") && "production-completion-past") || - (dayjs().add(1, "day").isSame(dayjs(card.scheduled_completion), "day") && "production-completion-soon")); + !!card?.metadata?.scheduled_completion && + ((dayjs().isSameOrAfter(dayjs(card.metadata.scheduled_completion), "day") && "production-completion-past") || + (dayjs().add(1, "day").isSame(dayjs(card.metadata.scheduled_completion), "day") && "production-completion-soon")); + + const totalHrs = card + ? card.metadata.labhrs.aggregate.sum.mod_lb_hrs + card.metadata.larhrs.aggregate.sum.mod_lb_hrs + : 0; - const totalHrs = card.labhrs.aggregate.sum.mod_lb_hrs + card.larhrs.aggregate.sum.mod_lb_hrs; const bgColor = cardColor(bodyshop.ssbuckets, totalHrs); return ( - {card.suspended && } - {card.iouparent && ( + {card.metadata.suspended && } + {card.metadata.iouparent && ( )} - {card.ro_number || t("general.labels.na")} + {card.metadata.ro_number || t("general.labels.na")} @@ -103,7 +108,7 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett {cardSettings && cardSettings.ownr_nm && ( {cardSettings && cardSettings.compact ? ( -
{`${card.ownr_ln || ""} ${card.ownr_co_nm || ""}`}
+
{`${card.metadata.ownr_ln || ""} ${card.metadata.ownr_co_nm || ""}`}
) : (
@@ -112,18 +117,18 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett )} -
{`${card.v_model_yr || ""} ${ - card.v_make_desc || "" - } ${card.v_model_desc || ""}`}
+
{`${card.metadata.v_model_yr || ""} ${ + card.metadata.v_make_desc || "" + } ${card.metadata.v_model_desc || ""}`}
- {cardSettings && cardSettings.ins_co_nm && card.ins_co_nm && ( + {cardSettings && cardSettings.ins_co_nm && card.metadata.ins_co_nm && ( -
{card.ins_co_nm || ""}
+
{card.metadata.ins_co_nm || ""}
)} - {cardSettings && cardSettings.clm_no && card.clm_no && ( + {cardSettings && cardSettings.clm_no && card.metadata.clm_no && ( -
{card.clm_no || ""}
+
{card.metadata.clm_no || ""}
)} @@ -132,7 +137,7 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett {`B: ${ employee_body ? `${employee_body.first_name.substr(0, 3)} ${employee_body.last_name.charAt(0)}` : "" - } ${card.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`} + } ${card.metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`} {`P: ${ employee_prep ? `${employee_prep.first_name.substr(0, 3)} ${employee_prep.last_name.charAt(0)}` : "" }`} @@ -140,7 +145,7 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett employee_refinish ? `${employee_refinish.first_name.substr(0, 3)} ${employee_refinish.last_name.charAt(0)}` : "" - } ${card.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`} + } ${card.metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`} {`C: ${ employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : "" }`} @@ -151,38 +156,38 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett {`B: ${ - card.labhrs.aggregate.sum.mod_lb_hrs || "?" + card.metadata.labhrs.aggregate.sum.mod_lb_hrs || "?" } hrs`} {`R: ${ - card.larhrs.aggregate.sum.mod_lb_hrs || "?" + card.metadata.larhrs.aggregate.sum.mod_lb_hrs || "?" } hrs`} )} */} - {cardSettings && cardSettings.actual_in && card.actual_in && ( + {cardSettings && cardSettings.actual_in && card.metadata.actual_in && ( - {card.actual_in} + {card.metadata.actual_in} )} - {cardSettings && cardSettings.scheduled_completion && card.scheduled_completion && ( + {cardSettings && cardSettings.scheduled_completion && card.metadata.scheduled_completion && ( - {card.scheduled_completion} + {card.metadata.scheduled_completion} )} - {cardSettings && cardSettings.ats && card.alt_transport && ( + {cardSettings && cardSettings.ats && card.metadata.alt_transport && ( -
{card.alt_transport || ""}
+
{card.metadata.alt_transport || ""}
)} {cardSettings && cardSettings.sublets && ( - + )} {cardSettings && cardSettings.production_note && ( @@ -192,7 +197,7 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett )} {cardSettings && cardSettings.partsstatus && ( - + )}
diff --git a/client/src/components/production-board-kanban/production-board-kanban.component.jsx b/client/src/components/production-board-kanban/production-board-kanban.component.jsx index f191bd339..3d3bc97a5 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.component.jsx +++ b/client/src/components/production-board-kanban/production-board-kanban.component.jsx @@ -1,6 +1,6 @@ import { SyncOutlined } from "@ant-design/icons"; import { useApolloClient } from "@apollo/client"; -import Board, { moveCard } from "@asseinfo/react-kanban"; +import Board from "../../components/trello-board/index"; import { Button, Grid, notification, Space, Statistic } from "antd"; import { PageHeader } from "@ant-design/pro-layout"; import React, { useEffect, useState } from "react"; @@ -42,9 +42,7 @@ export function ProductionBoardKanbanComponent({ insertAuditTrail, associationSettings }) { - const [boardLanes, setBoardLanes] = useState({ - columns: [{ id: "Loading...", title: "Loading...", cards: [] }] - }); + const [boardLanes, setBoardLanes] = useState({ lanes: [] }); const [filter, setFilter] = useState({ search: "", employeeId: null }); @@ -58,7 +56,7 @@ export function ProductionBoardKanbanComponent({ filter ); - boardData.columns = boardData.columns.map((d) => { + boardData.lanes = boardData.lanes.map((d) => { return { ...d, title: `${d.title} (${d.cards.length})` }; }); setBoardLanes(boardData); @@ -67,63 +65,55 @@ export function ProductionBoardKanbanComponent({ const client = useApolloClient(); - const handleDragEnd = async (card, source, destination) => { + const handleDragEnd = async (cardId, sourceLaneId, targetLaneId, position, cardDetails) => { logImEXEvent("kanban_drag_end"); setIsMoving(true); - setBoardLanes(moveCard(boardLanes, source, destination)); - const sameColumnTransfer = source.fromColumnId === destination.toColumnId; - const sourceColumn = boardLanes.columns.find((x) => x.id === source.fromColumnId); - const destinationColumn = boardLanes.columns.find((x) => x.id === destination.toColumnId); - const movedCardWillBeFirst = destination.toPosition === 0; + const sameColumnTransfer = sourceLaneId === targetLaneId; + const sourceLane = boardLanes.lanes.find((lane) => lane.id === sourceLaneId); + const targetLane = boardLanes.lanes.find((lane) => lane.id === targetLaneId); - const movedCardWillBeLast = destinationColumn.cards.length - destination.toPosition < 1; + const movedCardWillBeFirst = position === 0; + const movedCardWillBeLast = targetLane.cards.length - position < 1; - const lastCardInDestinationColumn = destinationColumn.cards[destinationColumn.cards.length - 1]; + const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1]; - const oldChildCard = sourceColumn.cards[source.fromPosition + 1]; + const oldChildCard = sourceLane.cards[position + 1]; const newChildCard = movedCardWillBeLast ? null - : destinationColumn.cards[ - sameColumnTransfer - ? source.fromPosition - destination.toPosition > 0 - ? destination.toPosition - : destination.toPosition + 1 - : destination.toPosition - ]; + : targetLane.cards[sameColumnTransfer ? (position - position > 0 ? position : position + 1) : position]; - const oldChildCardNewParent = oldChildCard ? card.kanbanparent : null; + const oldChildCardNewParent = oldChildCard ? cardDetails.kanbanparent : null; let movedCardNewKanbanParent; if (movedCardWillBeFirst) { - //console.log("==> New Card is first."); movedCardNewKanbanParent = "-1"; } else if (movedCardWillBeLast) { - // console.log("==> New Card is last."); - movedCardNewKanbanParent = lastCardInDestinationColumn.id; + movedCardNewKanbanParent = lastCardInTargetLane.id; } else if (!!newChildCard) { - // console.log("==> New Card is somewhere in the middle"); movedCardNewKanbanParent = newChildCard.kanbanparent; } else { console.log("==> !!!!!!Couldn't find a parent.!!!! <=="); } - const newChildCardNewParent = newChildCard ? card.id : null; + const newChildCardNewParent = newChildCard ? cardId : null; + const update = await client.mutate({ mutation: generate_UPDATE_JOB_KANBAN( oldChildCard ? oldChildCard.id : null, oldChildCardNewParent, - card.id, + cardId, movedCardNewKanbanParent, - destination.toColumnId, + targetLaneId, newChildCard ? newChildCard.id : null, newChildCardNewParent ) }); + insertAuditTrail({ - jobid: card.id, - operation: AuditTrailMapping.jobstatuschange(destination.toColumnId), + jobid: cardId, + operation: AuditTrailMapping.jobstatuschange(targetLaneId), type: "jobstatuschange" }); @@ -134,6 +124,8 @@ export function ProductionBoardKanbanComponent({ }) }); } + + setIsMoving(false); }; const totalHrs = data @@ -214,7 +206,6 @@ export function ProductionBoardKanbanComponent({ return ( - @@ -234,18 +225,20 @@ export function ProductionBoardKanbanComponent({ } /> - {cardSettings.cardcolor && } - ProductionBoardCard(technician, card, bodyshop, cardSettings)} - onCardDragEnd={handleDragEnd} + data={boardLanes} + draggable + canAddLanes + handleDragEnd={handleDragEnd} + editable + style={{ height: "100%", backgroundColor: "transparent" }} + renameLane + components={{ + Card: (cardProps) => ProductionBoardCard({ card: cardProps, technician, bodyshop, cardSettings }) + }} /> diff --git a/client/src/components/production-board-kanban/production-board-kanban.utils.js b/client/src/components/production-board-kanban/production-board-kanban.utils.js index 74b500143..ab286794f 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.utils.js +++ b/client/src/components/production-board-kanban/production-board-kanban.utils.js @@ -18,8 +18,8 @@ const sortByParentId = (arr) => { //console.log("sortByParentId -> byParentsIdsList", byParentsIdsList); while (byParentsIdsList[parentId]) { - sortedList.push(...byParentsIdsList[parentId]); //Spread in the whole list in case several items have the same parents. - parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length -1].id; //Grab the ID from the last one. + sortedList.push(...byParentsIdsList[parentId]); //Spread in the whole list in case several items have the same parents. + parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length - 1].id; //Grab the ID from the last one. } if (byParentsIdsList["null"]) byParentsIdsList["null"].map((i) => sortedList.push(i)); @@ -40,15 +40,13 @@ const sortByParentId = (arr) => { export const createBoardData = (AllStatuses, Jobs, filter) => { const { search, employeeId } = filter; - const boardLanes = { - columns: AllStatuses.map((s) => { - return { - id: s, - title: s, - cards: [] - }; - }) - }; + const lanes = AllStatuses.map((s) => { + return { + id: s, + title: s, + cards: [] + }; + }); const filteredJobs = (search === "" || !search) && !employeeId @@ -75,16 +73,25 @@ export const createBoardData = (AllStatuses, Jobs, filter) => { Object.keys(DataGroupedByStatus).map((statusGroupKey) => { try { - const needle = boardLanes.columns.find((l) => l.id === statusGroupKey); - if (!needle?.cards) return null; - needle.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]); + const lane = lanes.find((l) => l.id === statusGroupKey); + if (!lane?.cards) return null; + lane.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]).map((job) => { + const { id, title, description, due_date, ...metadata } = job; + return { + id, + title, + description, + label: job.due_date || "", + metadata + }; + }); } catch (error) { console.log("Error while creating board card", error); } return null; }); - return boardLanes; + return { lanes }; }; const CheckSearch = (search, job) => { diff --git a/client/src/components/production-list-columns/production-list-columns.productionnote.component.jsx b/client/src/components/production-list-columns/production-list-columns.productionnote.component.jsx index b5f5a1556..0119b8cc1 100644 --- a/client/src/components/production-list-columns/production-list-columns.productionnote.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.productionnote.component.jsx @@ -18,7 +18,8 @@ const mapDispatchToProps = (dispatch) => ({ function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) { const { t } = useTranslation(); - + console.log("RECORD"); + console.dir(record); const [note, setNote] = useState((record.production_vars && record.production_vars.note) || ""); const [open, setOpen] = useState(false); diff --git a/client/src/components/production-sublets-manage/production-sublets-manage.component.jsx b/client/src/components/production-sublets-manage/production-sublets-manage.component.jsx index a7a9ddad9..55aca7a8b 100644 --- a/client/src/components/production-sublets-manage/production-sublets-manage.component.jsx +++ b/client/src/components/production-sublets-manage/production-sublets-manage.component.jsx @@ -9,6 +9,8 @@ export default function ProductionSubletsManageComponent({ subletJobLines }) { const { t } = useTranslation(); const [updateJobLine] = useMutation(UPDATE_JOB_LINE_SUBLET); const [loading, setLoading] = useState(false); + console.log("subletJobLines"); + console.dir(subletJobLines); const subletCount = useMemo(() => { return { total: subletJobLines.filter((s) => !s.sublet_ignored).length, diff --git a/client/src/components/trello-board/components/AddCardLink.jsx b/client/src/components/trello-board/components/AddCardLink.jsx new file mode 100644 index 000000000..df259a9d9 --- /dev/null +++ b/client/src/components/trello-board/components/AddCardLink.jsx @@ -0,0 +1,11 @@ +import React from "react"; +import { AddCardLink } from "../styles/Base"; +import { useTranslation } from "react-i18next"; + +const AddCardLinkComponent = ({ onClick, laneId }) => { + const { t } = useTranslation(); + + return {t("trello.labels.add_card")}; +}; + +export default AddCardLinkComponent; diff --git a/client/src/components/trello-board/components/Card.jsx b/client/src/components/trello-board/components/Card.jsx new file mode 100644 index 000000000..d4ef6265d --- /dev/null +++ b/client/src/components/trello-board/components/Card.jsx @@ -0,0 +1,112 @@ +import React, { useCallback } from "react"; +import PropTypes from "prop-types"; + +import { CardHeader, CardRightContent, CardTitle, Detail, Footer, MovableCardWrapper } from "../styles/Base"; +import InlineInput from "../widgets/InlineInput.jsx"; +import Tag from "./Card/Tag.jsx"; +import DeleteButton from "../widgets/DeleteButton.jsx"; +import { useTranslation } from "react-i18next"; + +const Card = ({ + showDeleteButton = true, + onDelete = () => {}, + onClick = () => {}, + style = {}, + tagStyle = {}, + className = "", + id, + title = "no title", + label = "", + description = "", + tags = [], + cardDraggable, + editable, + onChange +}) => { + const { t } = useTranslation(); + + const handleDelete = useCallback( + (e) => { + onDelete(); + e.stopPropagation(); + }, + [onDelete] + ); + + const updateCard = (card) => { + onChange({ ...card, id }); + }; + + return ( + + + + {editable ? ( + updateCard({ title: value })} + /> + ) : ( + title + )} + + + {editable ? ( + updateCard({ label: value })} + /> + ) : ( + label + )} + + {showDeleteButton && } + + + {editable ? ( + updateCard({ description: value })} + /> + ) : ( + description + )} + + {tags && tags.length > 0 && ( +
+ {tags.map((tag) => ( + + ))} +
+ )} +
+ ); +}; + +Card.propTypes = { + showDeleteButton: PropTypes.bool, + onDelete: PropTypes.func, + onClick: PropTypes.func, + style: PropTypes.object, + tagStyle: PropTypes.object, + className: PropTypes.string, + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + label: PropTypes.string, + description: PropTypes.string, + tags: PropTypes.array, + cardDraggable: PropTypes.bool, + editable: PropTypes.bool, + onChange: PropTypes.func.isRequired +}; + +export default Card; diff --git a/client/src/components/trello-board/components/Card/Tag.jsx b/client/src/components/trello-board/components/Card/Tag.jsx new file mode 100644 index 000000000..78bd78063 --- /dev/null +++ b/client/src/components/trello-board/components/Card/Tag.jsx @@ -0,0 +1,21 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { TagSpan } from "../../styles/Base"; + +const Tag = ({ title, color, bgcolor, tagStyle, ...otherProps }) => { + const style = { color: color || "white", backgroundColor: bgcolor || "orange", ...tagStyle }; + return ( + + {title} + + ); +}; + +Tag.propTypes = { + title: PropTypes.string.isRequired, + color: PropTypes.string, + bgcolor: PropTypes.string, + tagStyle: PropTypes.object +}; + +export default Tag; diff --git a/client/src/components/trello-board/components/Lane/LaneFooter.jsx b/client/src/components/trello-board/components/Lane/LaneFooter.jsx new file mode 100644 index 000000000..ccf6dcdcb --- /dev/null +++ b/client/src/components/trello-board/components/Lane/LaneFooter.jsx @@ -0,0 +1,9 @@ +import React from "react"; +import { LaneFooter } from "../../styles/Base"; +import { CollapseBtn, ExpandBtn } from "../../styles/Elements"; + +const LaneFooterComponent = ({ onClick, collapsed }) => ( + {collapsed ? : } +); + +export default LaneFooterComponent; diff --git a/client/src/components/trello-board/components/Lane/LaneHeader.jsx b/client/src/components/trello-board/components/Lane/LaneHeader.jsx new file mode 100644 index 000000000..d61afcbf1 --- /dev/null +++ b/client/src/components/trello-board/components/Lane/LaneHeader.jsx @@ -0,0 +1,64 @@ +import React from "react"; +import PropTypes from "prop-types"; +import InlineInput from "../../widgets/InlineInput.jsx"; +import { LaneHeader, RightContent, Title } from "../../styles/Base"; +import LaneMenu from "./LaneHeader/LaneMenu.jsx"; +import { useTranslation } from "react-i18next"; + +const LaneHeaderComponent = ({ + updateTitle, + canAddLanes, + onDelete, + onDoubleClick, + editLaneTitle, + label, + title, + titleStyle, + labelStyle, + laneDraggable +}) => { + const { t } = useTranslation(); + + return ( + + + {editLaneTitle ? ( + <InlineInput + value={title} + border + placeholder={t("trello.labels.title")} + resize="vertical" + onSave={updateTitle} + /> + ) : ( + title + )} + + {label && ( + + {label} + + )} + {canAddLanes && } + + ); +}; + +LaneHeaderComponent.propTypes = { + updateTitle: PropTypes.func, + editLaneTitle: PropTypes.bool, + canAddLanes: PropTypes.bool, + laneDraggable: PropTypes.bool, + label: PropTypes.string, + title: PropTypes.string, + onDelete: PropTypes.func, + onDoubleClick: PropTypes.func +}; + +LaneHeaderComponent.defaultProps = { + updateTitle: () => {}, + editLaneTitle: false, + canAddLanes: false +}; + +export default LaneHeaderComponent; diff --git a/client/src/components/trello-board/components/Lane/LaneHeader/LaneMenu.jsx b/client/src/components/trello-board/components/Lane/LaneHeader/LaneMenu.jsx new file mode 100644 index 000000000..0dc6610bd --- /dev/null +++ b/client/src/components/trello-board/components/Lane/LaneHeader/LaneMenu.jsx @@ -0,0 +1,41 @@ +import React from "react"; + +import { Popover } from "react-popopo"; + +import { CustomPopoverContainer, CustomPopoverContent } from "../../../styles/Base"; + +import { + DeleteWrapper, + GenDelButton, + LaneMenuContent, + LaneMenuHeader, + LaneMenuItem, + LaneMenuTitle, + MenuButton +} from "../../../styles/Elements"; +import { useTranslation } from "react-i18next"; + +const LaneMenu = ({ onDelete }) => { + const { t } = useTranslation(); + + return ( + ⋮} + > + + {t("trello.labels.lane_actions")} + + + + + + {t("trello.labels.delete_lane")} + + + ); +}; + +export default LaneMenu; diff --git a/client/src/components/trello-board/components/Loader.jsx b/client/src/components/trello-board/components/Loader.jsx new file mode 100644 index 000000000..52b55ea81 --- /dev/null +++ b/client/src/components/trello-board/components/Loader.jsx @@ -0,0 +1,13 @@ +import React from 'react' +import {LoaderDiv, LoadingBar} from '../styles/Loader' + +const Loader = () => ( + + + + + + +) + +export default Loader diff --git a/client/src/components/trello-board/components/NewCardForm.jsx b/client/src/components/trello-board/components/NewCardForm.jsx new file mode 100644 index 000000000..585cc202b --- /dev/null +++ b/client/src/components/trello-board/components/NewCardForm.jsx @@ -0,0 +1,53 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { CardForm, CardHeader, CardRightContent, CardTitle, CardWrapper, Detail } from "../styles/Base"; +import { AddButton, CancelButton } from "../styles/Elements"; +import EditableLabel from "../widgets/EditableLabel.jsx"; +import { useTranslation } from "react-i18next"; + +const NewCardForm = ({ onCancel, onAdd }) => { + const [state, setState] = useState({}); + const { t } = useTranslation(); + + const updateField = (field, value) => { + setState((prevState) => ({ ...prevState, [field]: value })); + }; + + const handleAdd = () => { + onAdd(state); + }; + + return ( + + + + + updateField("title", val)} + autoFocus + /> + + + updateField("label", val)} /> + + + + updateField("description", val)} + /> + + + {t("trello.labels.add_card")} + {t("trello.labels.cancel")} + + ); +}; + +NewCardForm.propTypes = { + onCancel: PropTypes.func.isRequired, + onAdd: PropTypes.func.isRequired +}; + +export default NewCardForm; diff --git a/client/src/components/trello-board/components/NewLaneForm.jsx b/client/src/components/trello-board/components/NewLaneForm.jsx new file mode 100644 index 000000000..b24a956cc --- /dev/null +++ b/client/src/components/trello-board/components/NewLaneForm.jsx @@ -0,0 +1,56 @@ +import React, { useRef } from "react"; +import PropTypes from "prop-types"; +import { LaneTitle, NewLaneButtons, Section } from "../styles/Base"; +import { AddButton, CancelButton } from "../styles/Elements"; +import NewLaneTitleEditor from "../widgets/NewLaneTitleEditor.jsx"; +import { v1 } from "uuid"; +import { useTranslation } from "react-i18next"; + +const NewLane = ({ onCancel, onAdd }) => { + const refInput = useRef(null); + const { t } = useTranslation(); + + const handleSubmit = () => { + onAdd({ + id: v1(), + title: getValue() + }); + }; + + const getValue = () => refInput.current.getValue(); + + const onClickOutside = (a, b, c) => { + if (getValue().length > 0) { + handleSubmit(); + } else { + onCancel(); + } + }; + + return ( +
+ + + + + {t("trello.labels.add_lane")} + {t("trello.labels.cancel")} + +
+ ); +}; + +NewLane.propTypes = { + onCancel: PropTypes.func.isRequired, + onAdd: PropTypes.func.isRequired +}; + +export default NewLane; diff --git a/client/src/components/trello-board/components/NewLaneSection.jsx b/client/src/components/trello-board/components/NewLaneSection.jsx new file mode 100644 index 000000000..edfbe5434 --- /dev/null +++ b/client/src/components/trello-board/components/NewLaneSection.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { NewLaneSection } from "../styles/Base"; +import { AddLaneLink } from "../styles/Elements"; +import { useTranslation } from "react-i18next"; + +const NewLaneSectionComponent = ({ onClick }) => { + const { t } = useTranslation(); + + return ( + + {t("trello.labels.add_lane")} + + ); +}; + +export default NewLaneSectionComponent; diff --git a/client/src/components/trello-board/components/index.js b/client/src/components/trello-board/components/index.js new file mode 100644 index 000000000..24858380d --- /dev/null +++ b/client/src/components/trello-board/components/index.js @@ -0,0 +1,24 @@ +import LaneHeader from "./Lane/LaneHeader"; +import LaneFooter from "./Lane/LaneFooter"; +import Card from "./Card"; +import Loader from "./Loader.jsx"; +import NewLaneForm from "./NewLaneForm.jsx"; +import NewCardForm from "./NewCardForm.jsx"; +import AddCardLink from "./AddCardLink"; +import NewLaneSection from "./NewLaneSection.jsx"; +import { BoardWrapper, GlobalStyle, ScrollableLane, Section } from "../styles/Base"; + +export default { + GlobalStyle, + BoardWrapper, + Loader, + ScrollableLane, + LaneHeader, + LaneFooter, + Section, + NewLaneForm, + NewLaneSection, + NewCardForm, + Card, + AddCardLink +}; diff --git a/client/src/components/trello-board/controllers/Board.jsx b/client/src/components/trello-board/controllers/Board.jsx new file mode 100644 index 000000000..879845521 --- /dev/null +++ b/client/src/components/trello-board/controllers/Board.jsx @@ -0,0 +1,19 @@ +import { BoardContainer } from "../index.jsx"; +import classNames from "classnames"; +import { useState } from "react"; +import { v1 } from "uuid"; + +const Board = ({ id, className, components, ...additionalProps }) => { + const [storeId] = useState(id || v1()); + + const allClassNames = classNames("react-trello-board", className || ""); + + return ( + <> + + + + ); +}; + +export default Board; diff --git a/client/src/components/trello-board/controllers/BoardContainer.jsx b/client/src/components/trello-board/controllers/BoardContainer.jsx new file mode 100644 index 000000000..eae0cd51c --- /dev/null +++ b/client/src/components/trello-board/controllers/BoardContainer.jsx @@ -0,0 +1,294 @@ +import React, { Component } from "react"; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import Container from "../dnd/Container"; +import Draggable from "../dnd/Draggable"; +import PropTypes from "prop-types"; +import pick from "lodash/pick"; +import isEqual from "lodash/isEqual"; +import Lane from "./Lane"; +import { PopoverWrapper } from "react-popopo"; + +import * as actions from "../../../redux/trello/trello.actions.js"; + +class BoardContainer extends Component { + state = { + addLaneMode: false + }; + + get groupName() { + const { id } = this.props; + return `TrelloBoard${id}`; + } + + componentDidMount() { + const { actions, eventBusHandle } = this.props; + actions.loadBoard(this.props.data); + if (eventBusHandle) { + this.wireEventBus(); + } + } + + componentDidUpdate(prevProps) { + const { data, reducerData, onDataChange, actions } = this.props; + + if (this.props.reducerData && !isEqual(reducerData, prevProps.reducerData)) { + onDataChange(this.props.reducerData); + } + + if (data && !isEqual(data, prevProps.data)) { + actions.loadBoard(data); + onDataChange(data); + } + } + + onDragStart = ({ payload }) => { + const { handleLaneDragStart } = this.props; + handleLaneDragStart(payload.id); + }; + + onLaneDrop = ({ removedIndex, addedIndex, payload }) => { + const { actions, handleLaneDragEnd } = this.props; + if (removedIndex !== addedIndex) { + actions.moveLane({ oldIndex: removedIndex, newIndex: addedIndex }); + handleLaneDragEnd(removedIndex, addedIndex, payload); + } + }; + + getCardDetails = (laneId, cardIndex) => { + return this.props.reducerData.lanes.find((lane) => lane.id === laneId).cards[cardIndex]; + }; + + getLaneDetails = (index) => { + return this.props.reducerData.lanes[index]; + }; + + wireEventBus = () => { + const { actions, eventBusHandle } = this.props; + let eventBus = { + publish: (event) => { + switch (event.type) { + case "ADD_CARD": + return actions.addCard({ laneId: event.laneId, card: event.card }); + case "UPDATE_CARD": + return actions.updateCard({ laneId: event.laneId, card: event.card }); + case "REMOVE_CARD": + return actions.removeCard({ laneId: event.laneId, cardId: event.cardId }); + case "REFRESH_BOARD": + return actions.loadBoard(event.data); + case "MOVE_CARD": + return actions.moveCardAcrossLanes({ + fromLaneId: event.fromLaneId, + toLaneId: event.toLaneId, + cardId: event.cardId, + index: event.index + }); + case "UPDATE_CARDS": + return actions.updateCards({ laneId: event.laneId, cards: event.cards }); + case "UPDATE_CARD": + return actions.updateCard({ laneId: event.laneId, updatedCard: event.card }); + case "UPDATE_LANES": + return actions.updateLanes(event.lanes); + case "UPDATE_LANE": + return actions.updateLane(event.lane); + default: + return; + } + } + }; + eventBusHandle(eventBus); + }; + + // + add + hideEditableLane = () => { + this.setState({ addLaneMode: false }); + }; + + showEditableLane = () => { + this.setState({ addLaneMode: true }); + }; + + addNewLane = (params) => { + this.hideEditableLane(); + this.props.actions.addLane(params); + this.props.onLaneAdd(params); + }; + + render() { + const { + id, + components, + reducerData, + draggable, + laneDraggable, + laneDragClass, + laneDropClass, + style, + onDataChange, + onCardAdd, + onCardUpdate, + onCardClick, + onBeforeCardDelete, + onCardDelete, + onLaneScroll, + onLaneClick, + onLaneAdd, + onLaneDelete, + onLaneUpdate, + editable, + canAddLanes, + laneStyle, + onCardMoveAcrossLanes, + t, + ...otherProps + } = this.props; + + const { addLaneMode } = this.state; + // Stick to whitelisting attributes to segregate board and lane props + const passthroughProps = pick(this.props, [ + "onCardMoveAcrossLanes", + "onLaneScroll", + "onLaneDelete", + "onLaneUpdate", + "onCardClick", + "onBeforeCardDelete", + "onCardDelete", + "onCardAdd", + "onCardUpdate", + "onLaneClick", + "laneSortFunction", + "draggable", + "laneDraggable", + "cardDraggable", + "collapsibleLanes", + "canAddLanes", + "hideCardDeleteIcon", + "tagStyle", + "handleDragStart", + "handleDragEnd", + "cardDragClass", + "editLaneTitle", + "t" + ]); + + return ( + + + this.getLaneDetails(index)} + groupName={this.groupName} + > + {reducerData.lanes.map((lane, index) => { + const { id, droppable, ...otherProps } = lane; + const laneToRender = ( + + ); + return draggable && laneDraggable ? {laneToRender} : laneToRender; + })} + + + {canAddLanes && ( + + {editable && !addLaneMode ? ( + + ) : ( + addLaneMode && + )} + + )} + + ); + } +} + +BoardContainer.propTypes = { + id: PropTypes.string, + components: PropTypes.object, + actions: PropTypes.object, + data: PropTypes.object.isRequired, + reducerData: PropTypes.object, + onDataChange: PropTypes.func, + eventBusHandle: PropTypes.func, + onLaneScroll: PropTypes.func, + onCardClick: PropTypes.func, + onBeforeCardDelete: PropTypes.func, + onCardDelete: PropTypes.func, + onCardAdd: PropTypes.func, + onCardUpdate: PropTypes.func, + onLaneAdd: PropTypes.func, + onLaneDelete: PropTypes.func, + onLaneClick: PropTypes.func, + onLaneUpdate: PropTypes.func, + laneSortFunction: PropTypes.func, + draggable: PropTypes.bool, + collapsibleLanes: PropTypes.bool, + editable: PropTypes.bool, + canAddLanes: PropTypes.bool, + hideCardDeleteIcon: PropTypes.bool, + handleDragStart: PropTypes.func, + handleDragEnd: PropTypes.func, + handleLaneDragStart: PropTypes.func, + handleLaneDragEnd: PropTypes.func, + style: PropTypes.object, + tagStyle: PropTypes.object, + laneDraggable: PropTypes.bool, + cardDraggable: PropTypes.bool, + cardDragClass: PropTypes.string, + laneDragClass: PropTypes.string, + laneDropClass: PropTypes.string, + onCardMoveAcrossLanes: PropTypes.func.isRequired +}; + +BoardContainer.defaultProps = { + t: (v) => v, + onDataChange: () => {}, + handleDragStart: () => {}, + handleDragEnd: () => {}, + handleLaneDragStart: () => {}, + handleLaneDragEnd: () => {}, + onCardUpdate: () => {}, + onLaneAdd: () => {}, + onLaneDelete: () => {}, + onCardMoveAcrossLanes: () => {}, + onLaneUpdate: () => {}, + editable: false, + canAddLanes: false, + hideCardDeleteIcon: false, + draggable: false, + collapsibleLanes: false, + laneDraggable: true, + cardDraggable: true, + cardDragClass: "react_trello_dragClass", + laneDragClass: "react_trello_dragLaneClass", + laneDropClass: "" +}; + +const mapStateToProps = (state) => { + return state.trello.lanes ? { reducerData: state.trello } : {}; +}; + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators({ ...actions }, dispatch) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(BoardContainer); diff --git a/client/src/components/trello-board/controllers/Lane.jsx b/client/src/components/trello-board/controllers/Lane.jsx new file mode 100644 index 000000000..30802dcf7 --- /dev/null +++ b/client/src/components/trello-board/controllers/Lane.jsx @@ -0,0 +1,328 @@ +import React, { Component } from "react"; +import classNames from "classnames"; +import PropTypes from "prop-types"; +import { bindActionCreators } from "redux"; +import { connect } from "react-redux"; +import isEqual from "lodash/isEqual"; +import cloneDeep from "lodash/cloneDeep"; +import { v1 } from "uuid"; + +import Container from "../dnd/Container.jsx"; +import Draggable from "../dnd/Draggable.jsx"; + +import * as actions from "../../../redux/trello/trello.actions.js"; + +class Lane extends Component { + state = { + loading: false, + currentPage: this.props.currentPage, + addCardMode: false, + collapsed: false, + isDraggingOver: false + }; + + get groupName() { + const { boardId } = this.props; + return `TrelloBoard${boardId}Lane`; + } + + handleScroll = (evt) => { + const node = evt.target; + const elemScrollPosition = node.scrollHeight - node.scrollTop - node.clientHeight; + const { onLaneScroll } = this.props; + // In some browsers and/or screen sizes a decimal rest value between 0 and 1 exists, so it should be checked on < 1 instead of < 0 + if (elemScrollPosition < 1 && onLaneScroll && !this.state.loading) { + const { currentPage } = this.state; + this.setState({ loading: true }); + const nextPage = currentPage + 1; + onLaneScroll(nextPage, this.props.id).then((moreCards) => { + if ((moreCards || []).length > 0) { + this.props.actions.paginateLane({ + laneId: this.props.id, + newCards: moreCards, + nextPage: nextPage + }); + } + this.setState({ loading: false }); + }); + } + }; + + sortCards(cards, sortFunction) { + if (!cards) return []; + if (!sortFunction) return cards; + return cards.concat().sort(function (card1, card2) { + return sortFunction(card1, card2); + }); + } + + laneDidMount = (node) => { + if (node) { + node.addEventListener("scroll", this.handleScroll); + } + }; + + UNSAFE_componentWillReceiveProps(nextProps) { + if (!isEqual(this.props.cards, nextProps.cards)) { + this.setState({ + currentPage: nextProps.currentPage + }); + } + } + + removeCard = (cardId) => { + if (this.props.onBeforeCardDelete && typeof this.props.onBeforeCardDelete === "function") { + this.props.onBeforeCardDelete(() => { + this.props.onCardDelete && this.props.onCardDelete(cardId, this.props.id); + this.props.actions.removeCard({ laneId: this.props.id, cardId: cardId }); + }); + } else { + this.props.onCardDelete && this.props.onCardDelete(cardId, this.props.id); + this.props.actions.removeCard({ laneId: this.props.id, cardId: cardId }); + } + }; + + handleCardClick = (e, card) => { + const { onCardClick } = this.props; + onCardClick && onCardClick(card.id, card.metadata, card.laneId); + e.stopPropagation(); + }; + + showEditableCard = () => { + this.setState({ addCardMode: true }); + }; + + hideEditableCard = () => { + this.setState({ addCardMode: false }); + }; + + addNewCard = (params) => { + const laneId = this.props.id; + const id = v1(); + this.hideEditableCard(); + let card = { id, ...params }; + this.props.actions.addCard({ laneId, card }); + this.props.onCardAdd(card, laneId); + }; + + onDragStart = ({ payload }) => { + const { handleDragStart } = this.props; + handleDragStart && handleDragStart(payload.id, payload.laneId); + }; + + shouldAcceptDrop = (sourceContainerOptions) => { + return this.props.droppable && sourceContainerOptions.groupName === this.groupName; + }; + + onDragEnd = (laneId, result) => { + const { handleDragEnd } = this.props; + const { addedIndex, payload } = result; + + if (this.state.isDraggingOver) { + this.setState({ isDraggingOver: false }); + } + + if (addedIndex != null) { + const newCard = { ...cloneDeep(payload), laneId }; + const response = handleDragEnd ? handleDragEnd(payload.id, payload.laneId, laneId, addedIndex, newCard) : true; + if (response === undefined || !!response) { + this.props.actions.moveCardAcrossLanes({ + fromLaneId: payload.laneId, + toLaneId: laneId, + cardId: payload.id, + index: addedIndex + }); + this.props.onCardMoveAcrossLanes(payload.laneId, laneId, payload.id, addedIndex); + } + return response; + } + }; + + updateCard = (updatedCard) => { + this.props.actions.updateCard({ laneId: this.props.id, card: updatedCard }); + this.props.onCardUpdate(this.props.id, updatedCard); + }; + + renderDragContainer = (isDraggingOver) => { + const { + id, + cards, + laneSortFunction, + editable, + hideCardDeleteIcon, + cardDraggable, + cardDragClass, + cardDropClass, + tagStyle, + cardStyle, + components, + t + } = this.props; + const { addCardMode, collapsed } = this.state; + + const showableCards = collapsed ? [] : cards; + + const cardList = this.sortCards(showableCards, laneSortFunction).map((card, idx) => { + const onDeleteCard = () => this.removeCard(card.id); + const cardToRender = ( + this.handleCardClick(e, card)} + onChange={(updatedCard) => this.updateCard(updatedCard)} + showDeleteButton={!hideCardDeleteIcon} + tagStyle={tagStyle} + cardDraggable={cardDraggable} + editable={editable} + {...card} + /> + ); + return cardDraggable && (!card.hasOwnProperty("draggable") || card.draggable) ? ( + {cardToRender} + ) : ( + {cardToRender} + ); + }); + + return ( + + this.onDragEnd(id, e)} + onDragEnter={() => this.setState({ isDraggingOver: true })} + onDragLeave={() => this.setState({ isDraggingOver: false })} + shouldAcceptDrop={this.shouldAcceptDrop} + getChildPayload={(index) => this.props.getCardDetails(id, index)} + > + {cardList} + + {editable && !addCardMode && } + {addCardMode && } + + ); + }; + + removeLane = () => { + const { id } = this.props; + this.props.actions.removeLane({ laneId: id }); + this.props.onLaneDelete(id); + }; + + updateTitle = (value) => { + this.props.actions.updateLane({ id: this.props.id, title: value }); + this.props.onLaneUpdate(this.props.id, { title: value }); + }; + + renderHeader = (pickedProps) => { + const { components } = this.props; + return ( + + ); + }; + + toggleLaneCollapsed = () => { + this.props.collapsibleLanes && this.setState((state) => ({ collapsed: !state.collapsed })); + }; + + render() { + const { loading, isDraggingOver, collapsed } = this.state; + const { + id, + cards, + collapsibleLanes, + components, + onLaneClick, + onLaneScroll, + onCardClick, + onCardAdd, + onBeforeCardDelete, + onCardDelete, + onLaneDelete, + onLaneUpdate, + onCardUpdate, + onCardMoveAcrossLanes, + ...otherProps + } = this.props; + const allClassNames = classNames("react-trello-lane", this.props.className || ""); + const showFooter = collapsibleLanes && cards.length > 0; + return ( + onLaneClick && onLaneClick(id)} + draggable={false} + className={allClassNames} + > + {this.renderHeader({ id, cards, ...otherProps })} + {this.renderDragContainer(isDraggingOver)} + {loading && } + {showFooter && } + + ); + } +} + +Lane.propTypes = { + actions: PropTypes.object, + id: PropTypes.string.isRequired, + boardId: PropTypes.string, + title: PropTypes.node, + index: PropTypes.number, + laneSortFunction: PropTypes.func, + style: PropTypes.object, + cardStyle: PropTypes.object, + tagStyle: PropTypes.object, + titleStyle: PropTypes.object, + labelStyle: PropTypes.object, + cards: PropTypes.array, + label: PropTypes.string, + currentPage: PropTypes.number, + draggable: PropTypes.bool, + collapsibleLanes: PropTypes.bool, + droppable: PropTypes.bool, + onCardMoveAcrossLanes: PropTypes.func, + onCardClick: PropTypes.func, + onBeforeCardDelete: PropTypes.func, + onCardDelete: PropTypes.func, + onCardAdd: PropTypes.func, + onCardUpdate: PropTypes.func, + onLaneDelete: PropTypes.func, + onLaneUpdate: PropTypes.func, + onLaneClick: PropTypes.func, + onLaneScroll: PropTypes.func, + editable: PropTypes.bool, + laneDraggable: PropTypes.bool, + cardDraggable: PropTypes.bool, + cardDragClass: PropTypes.string, + cardDropClass: PropTypes.string, + canAddLanes: PropTypes.bool +}; + +Lane.defaultProps = { + style: {}, + titleStyle: {}, + labelStyle: {}, + label: undefined, + editable: false, + onLaneUpdate: () => {}, + onCardAdd: () => {}, + onCardUpdate: () => {} +}; + +const mapDispatchToProps = (dispatch) => ({ + actions: bindActionCreators(actions, dispatch) +}); + +export default connect(null, mapDispatchToProps)(Lane); diff --git a/client/src/components/trello-board/dnd/Container.jsx b/client/src/components/trello-board/dnd/Container.jsx new file mode 100644 index 000000000..6b6883a6d --- /dev/null +++ b/client/src/components/trello-board/dnd/Container.jsx @@ -0,0 +1,139 @@ +import React, {Component} from 'react' +import ReactDOM from 'react-dom' +import PropTypes from 'prop-types' +import container, {dropHandlers} from 'kuika-smooth-dnd' + +container.dropHandler = dropHandlers.reactDropHandler().handler +container.wrapChild = p => p // dont wrap children they will already be wrapped + +class Container extends Component { + constructor(props) { + super(props) + this.getContainerOptions = this.getContainerOptions.bind(this) + this.setRef = this.setRef.bind(this) + this.prevContainer = null + } + + componentDidMount() { + this.containerDiv = this.containerDiv || ReactDOM.findDOMNode(this) + this.prevContainer = this.containerDiv + this.container = container(this.containerDiv, this.getContainerOptions()) + } + + componentWillUnmount() { + this.container.dispose() + this.container = null + } + + componentDidUpdate() { + this.containerDiv = this.containerDiv || ReactDOM.findDOMNode(this) + if (this.containerDiv) { + if (this.prevContainer && this.prevContainer !== this.containerDiv) { + this.container.dispose() + this.container = container(this.containerDiv, this.getContainerOptions()) + this.prevContainer = this.containerDiv + } + } + } + + render() { + if (this.props.render) { + return this.props.render(this.setRef) + } else { + return ( +
+ {this.props.children} +
+ ) + } + } + + setRef(element) { + this.containerDiv = element + } + + getContainerOptions() { + const functionProps = {} + + if (this.props.onDragStart) { + functionProps.onDragStart = (...p) => this.props.onDragStart(...p) + } + + if (this.props.onDragEnd) { + functionProps.onDragEnd = (...p) => this.props.onDragEnd(...p) + } + + if (this.props.onDrop) { + functionProps.onDrop = (...p) => this.props.onDrop(...p) + } + + if (this.props.getChildPayload) { + functionProps.getChildPayload = (...p) => this.props.getChildPayload(...p) + } + + if (this.props.shouldAnimateDrop) { + functionProps.shouldAnimateDrop = (...p) => this.props.shouldAnimateDrop(...p) + } + + if (this.props.shouldAcceptDrop) { + functionProps.shouldAcceptDrop = (...p) => this.props.shouldAcceptDrop(...p) + } + + if (this.props.onDragEnter) { + functionProps.onDragEnter = (...p) => this.props.onDragEnter(...p) + } + + if (this.props.onDragLeave) { + functionProps.onDragLeave = (...p) => this.props.onDragLeave(...p) + } + + if (this.props.render) { + functionProps.render = (...p) => this.props.render(...p) + } + + if (this.props.onDropReady) { + functionProps.onDropReady = (...p) => this.props.onDropReady(...p) + } + + if (this.props.getGhostParent) { + functionProps.getGhostParent = (...p) => this.props.getGhostParent(...p) + } + + return Object.assign({}, this.props, functionProps) + } +} + +Container.propTypes = { + behaviour: PropTypes.oneOf(['move', 'copy', 'drag-zone']), + groupName: PropTypes.string, + orientation: PropTypes.oneOf(['horizontal', 'vertical']), + style: PropTypes.object, + dragHandleSelector: PropTypes.string, + className: PropTypes.string, + nonDragAreaSelector: PropTypes.string, + dragBeginDelay: PropTypes.number, + animationDuration: PropTypes.number, + autoScrollEnabled: PropTypes.string, + lockAxis: PropTypes.string, + dragClass: PropTypes.string, + dropClass: PropTypes.string, + onDragStart: PropTypes.func, + onDragEnd: PropTypes.func, + onDrop: PropTypes.func, + getChildPayload: PropTypes.func, + shouldAnimateDrop: PropTypes.func, + shouldAcceptDrop: PropTypes.func, + onDragEnter: PropTypes.func, + onDragLeave: PropTypes.func, + render: PropTypes.func, + getGhostParent: PropTypes.func, + removeOnDropOut: PropTypes.bool +} + +Container.defaultProps = { + behaviour: 'move', + orientation: 'vertical', + className: 'reactTrelloBoard' +} + +export default Container diff --git a/client/src/components/trello-board/dnd/Draggable.jsx b/client/src/components/trello-board/dnd/Draggable.jsx new file mode 100644 index 000000000..080422121 --- /dev/null +++ b/client/src/components/trello-board/dnd/Draggable.jsx @@ -0,0 +1,26 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import {constants} from 'kuika-smooth-dnd' + +const {wrapperClass} = constants + +class Draggable extends Component { + render() { + if (this.props.render) { + return React.cloneElement(this.props.render(), {className: wrapperClass}) + } + + const clsName = `${this.props.className ? this.props.className + ' ' : ''}` + return ( +
+ {this.props.children} +
+ ) + } +} + +Draggable.propTypes = { + render: PropTypes.func +} + +export default Draggable diff --git a/client/src/components/trello-board/helpers/LaneHelper.js b/client/src/components/trello-board/helpers/LaneHelper.js new file mode 100644 index 000000000..b13a65483 --- /dev/null +++ b/client/src/components/trello-board/helpers/LaneHelper.js @@ -0,0 +1,135 @@ +import update from "immutability-helper"; + +const LaneHelper = { + initialiseLanes: (state, { lanes }) => { + const newLanes = lanes.map((lane) => { + lane.currentPage = 1; + lane.cards && lane.cards.forEach((c) => (c.laneId = lane.id)); + return lane; + }); + return update(state, { lanes: { $set: newLanes } }); + }, + + paginateLane: (state, { laneId, newCards, nextPage }) => { + const updatedLanes = LaneHelper.appendCardsToLane(state, { laneId: laneId, newCards: newCards }); + updatedLanes.find((lane) => lane.id === laneId).currentPage = nextPage; + return update(state, { lanes: { $set: updatedLanes } }); + }, + + appendCardsToLane: (state, { laneId, newCards, index }) => { + const lane = state.lanes.find((lane) => lane.id === laneId); + newCards = newCards + .map((c) => update(c, { laneId: { $set: laneId } })) + .filter((c) => lane.cards.find((card) => card.id === c.id) == null); + return state.lanes.map((lane) => { + if (lane.id === laneId) { + if (index !== undefined) { + return update(lane, { cards: { $splice: [[index, 0, ...newCards]] } }); + } else { + const cardsToUpdate = [...lane.cards, ...newCards]; + return update(lane, { cards: { $set: cardsToUpdate } }); + } + } else { + return lane; + } + }); + }, + + appendCardToLane: (state, { laneId, card, index }) => { + const newLanes = LaneHelper.appendCardsToLane(state, { laneId: laneId, newCards: [card], index }); + return update(state, { lanes: { $set: newLanes } }); + }, + + addLane: (state, lane) => { + const newLane = { cards: [], ...lane }; + return update(state, { lanes: { $push: [newLane] } }); + }, + + updateLane: (state, updatedLane) => { + const newLanes = state.lanes.map((lane) => { + if (updatedLane.id === lane.id) { + return { ...lane, ...updatedLane }; + } else { + return lane; + } + }); + return update(state, { lanes: { $set: newLanes } }); + }, + + removeCardFromLane: (state, { laneId, cardId }) => { + const lanes = state.lanes.map((lane) => { + if (lane.id === laneId) { + let newCards = lane.cards.filter((card) => card.id !== cardId); + return update(lane, { cards: { $set: newCards } }); + } else { + return lane; + } + }); + return update(state, { lanes: { $set: lanes } }); + }, + + moveCardAcrossLanes: (state, { fromLaneId, toLaneId, cardId, index }) => { + let cardToMove = null; + const interimLanes = state.lanes.map((lane) => { + if (lane.id === fromLaneId) { + cardToMove = lane.cards.find((card) => card.id === cardId); + const newCards = lane.cards.filter((card) => card.id !== cardId); + return update(lane, { cards: { $set: newCards } }); + } else { + return lane; + } + }); + const updatedState = update(state, { lanes: { $set: interimLanes } }); + return LaneHelper.appendCardToLane(updatedState, { + laneId: toLaneId, + card: cardToMove, + index: index + }); + }, + + updateCardsForLane: (state, { laneId, cards }) => { + const lanes = state.lanes.map((lane) => { + if (lane.id === laneId) { + return update(lane, { cards: { $set: cards } }); + } else { + return lane; + } + }); + return update(state, { lanes: { $set: lanes } }); + }, + + updateCardForLane: (state, { laneId, card: updatedCard }) => { + const lanes = state.lanes.map((lane) => { + if (lane.id === laneId) { + const cards = lane.cards.map((card) => { + if (card.id === updatedCard.id) { + return { ...card, ...updatedCard }; + } else { + return card; + } + }); + return update(lane, { cards: { $set: cards } }); + } else { + return lane; + } + }); + return update(state, { lanes: { $set: lanes } }); + }, + + updateLanes: (state, lanes) => { + return { ...state, ...{ lanes: lanes } }; + }, + + moveLane: (state, { oldIndex, newIndex }) => { + const laneToMove = state.lanes[oldIndex]; + const tempState = update(state, { lanes: { $splice: [[oldIndex, 1]] } }); + return update(tempState, { lanes: { $splice: [[newIndex, 0, laneToMove]] } }); + }, + + removeLane: (state, { laneId }) => { + const updatedLanes = state.lanes.filter((lane) => lane.id !== laneId); + return update(state, { lanes: { $set: updatedLanes } }); + } +}; + +export default LaneHelper; diff --git a/client/src/components/trello-board/helpers/deprecationWarnings.js b/client/src/components/trello-board/helpers/deprecationWarnings.js new file mode 100644 index 000000000..41d203805 --- /dev/null +++ b/client/src/components/trello-board/helpers/deprecationWarnings.js @@ -0,0 +1,24 @@ +const REPLACE_TABLE = { + customLaneHeader: 'components.LaneHeader', + newLaneTemplate: 'components.NewLaneSection', + newCardTemplate: 'components.NewCardForm', + children: 'components.Card', + customCardLayout: 'components.Card', + addLaneTitle: '`t` function with key "Add another lane"', + addCardLink: '`t` function with key "Click to add card"' +} + +const warn = prop => { + const use = REPLACE_TABLE[prop] + console.warn( + `react-trello property '${prop}' is removed. Use '${use}' instead. More - https://github.com/rcdexta/react-trello/blob/master/UPGRADE.md` + ) +} + +export default props => { + Object.keys(REPLACE_TABLE).forEach(key => { + if (props.hasOwnProperty(key)) { + warn(key) + } + }) +} diff --git a/client/src/components/trello-board/index.jsx b/client/src/components/trello-board/index.jsx new file mode 100644 index 000000000..55826bdd2 --- /dev/null +++ b/client/src/components/trello-board/index.jsx @@ -0,0 +1,38 @@ +import React from "react"; + +import Draggable from "./dnd/Draggable.jsx"; +import Container from "./dnd/Container.jsx"; +import BoardContainer from "./controllers/BoardContainer.jsx"; +import Board from "./controllers/Board.jsx"; +import Lane from "./controllers/Lane.jsx"; +import deprecationWarnings from "./helpers/deprecationWarnings"; +import DefaultComponents from "./components"; + +import widgets from "./widgets/index"; + +import { StyleSheetManager } from "styled-components"; +import isPropValid from "@emotion/is-prop-valid"; + +export { Draggable, Container, BoardContainer, Lane, widgets }; + +export { DefaultComponents as components }; + +// Enhanced default export using arrow function for simplicity +const TrelloBoard = ({ components, ...otherProps }) => { + deprecationWarnings(otherProps); + + return ( + + ; + + ); +}; + +const shouldForwardProp = (propName, target) => { + if (typeof target === "string") { + return isPropValid(propName); + } + return true; +}; + +export default TrelloBoard; diff --git a/client/src/components/trello-board/styles/Base.js b/client/src/components/trello-board/styles/Base.js new file mode 100644 index 000000000..be2c630e4 --- /dev/null +++ b/client/src/components/trello-board/styles/Base.js @@ -0,0 +1,298 @@ +import { PopoverContainer, PopoverContent } from "react-popopo"; +import styled, { createGlobalStyle, css } from "styled-components"; + +export const GlobalStyle = createGlobalStyle` + .comPlainTextContentEditable { + -webkit-user-modify: read-write-plaintext-only; + cursor: text; + } + + .comPlainTextContentEditable--has-placeholder::before { + content: attr(placeholder); + opacity: 0.5; + color: inherit; + cursor: text; + } + + .react_trello_dragClass { + transform: rotate(3deg); + } + + .react_trello_dragLaneClass { + transform: rotate(3deg); + } + + .icon-overflow-menu-horizontal:before { + content: "\\E91F"; + } + + .icon-lg, .icon-sm { + color: #798d99; + } + + .icon-lg { + height: 32px; + font-size: 16px; + line-height: 32px; + width: 32px; + } +`; + +export const CustomPopoverContainer = styled(PopoverContainer)` + position: absolute; + right: 10px; + flex-flow: column nowrap; +`; + +export const CustomPopoverContent = styled(PopoverContent)` + visibility: hidden; + margin-top: -5px; + opacity: 0; + position: absolute; + z-index: 10; + box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3); + transition: all 0.3s ease 0ms; + border-radius: 3px; + min-width: 7em; + flex-flow: column nowrap; + background-color: #fff; + color: #000; + padding: 5px; + left: 50%; + transform: translateX(-50%); + + ${(props) => + props.active && + ` + visibility: visible; + opacity: 1; + transition-delay: 100ms; + `} &::before { + visibility: hidden; + } + + a { + color: rgba(255, 255, 255, 0.56); + padding: 0.5em 1em; + margin: 0; + text-decoration: none; + + &:hover { + background-color: #00bcd4 !important; + color: #37474f; + } + } +`; + +export const BoardWrapper = styled.div` + background-color: #3179ba; + overflow-y: hidden; + padding: 5px; + color: #393939; + display: flex; + flex-direction: row; + align-items: flex-start; + height: 100vh; +`; + +export const Header = styled.header` + margin-bottom: 10px; + display: flex; + flex-direction: row; + align-items: flex-start; +`; + +export const Section = styled.section` + background-color: #e3e3e3; + border-radius: 3px; + margin: 5px 5px; + position: relative; + padding: 10px; + display: inline-flex; + height: auto; + max-height: 90%; + flex-direction: column; +`; + +export const LaneHeader = styled(Header)` + margin-bottom: 0px; + ${(props) => + props.editLaneTitle && + css` + padding: 0px; + line-height: 30px; + `} ${(props) => + !props.editLaneTitle && + css` + padding: 0px 5px; + `}; +`; + +export const LaneFooter = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + position: relative; + height: 10px; +`; + +export const ScrollableLane = styled.div` + flex: 1; + overflow-y: auto; + min-width: 250px; + overflow-x: hidden; + align-self: center; + max-height: 90vh; + min-height: 100px; + margin-top: 10px; + flex-direction: column; + justify-content: space-between; +`; + +export const Title = styled.span` + font-weight: bold; + font-size: 15px; + line-height: 18px; + cursor: ${(props) => (props.draggable ? "grab" : `auto`)}; + width: 70%; +`; + +export const RightContent = styled.span` + width: 38%; + text-align: right; + padding-right: 10px; + font-size: 13px; +`; +export const CardWrapper = styled.article` + border-radius: 3px; + border-bottom: 1px solid #ccc; + background-color: #fff; + position: relative; + padding: 10px; + cursor: pointer; + max-width: 250px; + margin-bottom: 7px; + min-width: 230px; +`; + +export const MovableCardWrapper = styled(CardWrapper)` + &:hover { + background-color: #f0f0f0; + color: #000; + } +`; + +export const CardHeader = styled(Header)` + border-bottom: 1px solid #eee; + padding-bottom: 6px; + color: #000; +`; + +export const CardTitle = styled(Title)` + font-size: 14px; +`; + +export const CardRightContent = styled(RightContent)` + font-size: 10px; +`; + +export const Detail = styled.div` + font-size: 12px; + color: #4d4d4d; + white-space: pre-wrap; +`; + +export const Footer = styled.div` + border-top: 1px solid #eee; + padding-top: 6px; + text-align: right; + display: flex; + justify-content: flex-end; + flex-direction: row; + flex-wrap: wrap; +`; + +export const TagSpan = styled.span` + padding: 2px 3px; + border-radius: 3px; + margin: 2px 5px; + font-size: 70%; +`; + +export const AddCardLink = styled.a` + border-radius: 0 0 3px 3px; + color: #838c91; + display: block; + padding: 5px 2px; + margin-top: 10px; + position: relative; + text-decoration: none; + cursor: pointer; + + &:hover { + //background-color: #cdd2d4; + color: #4d4d4d; + text-decoration: underline; + } +`; + +export const LaneTitle = styled.div` + font-size: 15px; + width: 268px; + height: auto; +`; + +export const LaneSection = styled.section` + background-color: #2b6aa3; + border-radius: 3px; + margin: 5px; + position: relative; + padding: 5px; + display: inline-flex; + height: auto; + flex-direction: column; +`; + +export const NewLaneSection = styled(LaneSection)` + width: 200px; +`; + +export const NewLaneButtons = styled.div` + margin-top: 10px; +`; + +export const CardForm = styled.div` + background-color: #e3e3e3; +`; + +export const InlineInput = styled.textarea` + overflow-x: hidden; /* for Firefox (issue #5) */ + word-wrap: break-word; + min-height: 18px; + max-height: 112px; /* optional, but recommended */ + resize: none; + width: 100%; + height: 18px; + font-size: inherit; + font-weight: inherit; + line-height: inherit; + text-align: inherit; + background-color: transparent; + box-shadow: none; + box-sizing: border-box; + border-radius: 3px; + border: 0; + padding: 0 8px; + outline: 0; + + ${(props) => + props.border && + css` + &:focus { + box-shadow: inset 0 0 0 2px #0079bf; + } + `} &:focus { + background-color: white; + } +`; diff --git a/client/src/components/trello-board/styles/Elements.js b/client/src/components/trello-board/styles/Elements.js new file mode 100644 index 000000000..06e361d5c --- /dev/null +++ b/client/src/components/trello-board/styles/Elements.js @@ -0,0 +1,251 @@ +import styled from 'styled-components' +import {CardWrapper, MovableCardWrapper} from './Base' + +export const DeleteWrapper = styled.div` + text-align: center; + position: absolute; + top: -1px; + right: 2px; + cursor: pointer; +` + +export const GenDelButton = styled.button` + transition: all 0.5s ease; + display: inline-block; + border: none; + font-size: 15px; + height: 15px; + padding: 0; + margin-top: 5px; + text-align: center; + width: 15px; + background: inherit; + cursor: pointer; +` + +export const DelButton = styled.button` + transition: all 0.5s ease; + display: inline-block; + border: none; + font-size: 8px; + height: 15px; + line-height: 1px; + margin: 0 0 8px; + padding: 0; + text-align: center; + width: 15px; + background: inherit; + cursor: pointer; + opacity: 0; + + ${MovableCardWrapper}:hover & { + opacity: 1; + } +` + +export const MenuButton = styled.button` + transition: all 0.5s ease; + display: inline-block; + border: none; + outline: none; + font-size: 16px; + font-weight: bold; + height: 15px; + line-height: 1px; + margin: 0 0 8px; + padding: 0; + text-align: center; + width: 15px; + background: inherit; + cursor: pointer; +` + +export const LaneMenuHeader = styled.div` + position: relative; + margin-bottom: 4px; + text-align: center; +` + +export const LaneMenuContent = styled.div` + overflow-x: hidden; + overflow-y: auto; + padding: 0 12px 12px; +` + +export const LaneMenuItem = styled.div` + cursor: pointer; + display: block; + font-weight: 700; + padding: 6px 12px; + position: relative; + margin: 0 -12px; + text-decoration: none; + + &:hover { + background-color: #3179ba; + color: #fff; + } +` + +export const LaneMenuTitle = styled.span` + box-sizing: border-box; + color: #6b808c; + display: block; + line-height: 30px; + border-bottom: 1px solid rgba(9, 45, 66, 0.13); + margin: 0 6px; + overflow: hidden; + padding: 0 32px; + position: relative; + text-overflow: ellipsis; + white-space: nowrap; + z-index: 1; +` + +export const DeleteIcon = styled.span` + position: relative; + display: inline-block; + width: 4px; + height: 4px; + opacity: 1; + overflow: hidden; + border: 1px solid #83bd42; + border-radius: 50%; + padding: 4px; + background-color: #83bd42; + + ${CardWrapper}:hover & { + opacity: 1; + } + + &:hover::before, + &:hover::after { + background: red; + } + + &:before, + &:after { + content: ''; + position: absolute; + height: 2px; + width: 60%; + top: 45%; + left: 20%; + background: #fff; + border-radius: 5px; + } + + &:before { + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); + } + + &:after { + -webkit-transform: rotate(-45deg); + -moz-transform: rotate(-45deg); + -o-transform: rotate(-45deg); + transform: rotate(-45deg); + } +` + +export const ExpandCollapseBase = styled.span` + width: 36px; + margin: 0 auto; + font-size: 14px; + position: relative; + cursor: pointer; +` + +export const CollapseBtn = styled(ExpandCollapseBase)` + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + border-bottom: 7px solid #444; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-radius: 6px; + } + + &:after { + content: ''; + position: absolute; + left: 4px; + top: 4px; + border-bottom: 3px solid #e3e3e3; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + } +` + +export const ExpandBtn = styled(ExpandCollapseBase)` + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + border-top: 7px solid #444; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-radius: 6px; + } + + &:after { + content: ''; + position: absolute; + left: 4px; + top: 0px; + border-top: 3px solid #e3e3e3; + border-left: 3px solid transparent; + border-right: 3px solid transparent; + } +` + +export const AddButton = styled.button` + background: #5aac44; + color: #fff; + transition: background 0.3s ease; + min-height: 32px; + padding: 4px 16px; + vertical-align: top; + margin-top: 0; + margin-right: 8px; + font-weight: bold; + border-radius: 3px; + font-size: 14px; + cursor: pointer; + margin-bottom: 0; +` + +export const CancelButton = styled.button` + background: #999999; + color: #fff; + transition: background 0.3s ease; + min-height: 32px; + padding: 4px 16px; + vertical-align: top; + margin-top: 0; + font-weight: bold; + border-radius: 3px; + font-size: 14px; + cursor: pointer; + margin-bottom: 0; +` +export const AddLaneLink = styled.button` + background: #2b6aa3; + border: none; + color: #fff; + transition: background 0.3s ease; + min-height: 32px; + padding: 4px 16px; + vertical-align: top; + margin-top: 0; + margin-right: 0px; + border-radius: 4px; + font-size: 13px; + cursor: pointer; + margin-bottom: 0; +` diff --git a/client/src/components/trello-board/styles/Loader.js b/client/src/components/trello-board/styles/Loader.js new file mode 100644 index 000000000..2d7545399 --- /dev/null +++ b/client/src/components/trello-board/styles/Loader.js @@ -0,0 +1,43 @@ +import styled, {keyframes} from 'styled-components' + +const keyframeAnimation = keyframes` + 0% { + transform: scale(1); + } + 20% { + transform: scale(1, 2.2); + } + 40% { + transform: scale(1); + } +` +export const LoaderDiv = styled.div` + text-align: center; + margin: 15px 0; +` + +export const LoadingBar = styled.div` + display: inline-block; + margin: 0 2px; + width: 4px; + height: 18px; + border-radius: 4px; + animation: ${keyframeAnimation} 1s ease-in-out infinite; + background-color: #777; + + &:nth-child(1) { + animation-delay: 0.0001s; + } + + &:nth-child(2) { + animation-delay: 0.09s; + } + + &:nth-child(3) { + animation-delay: 0.18s; + } + + &:nth-child(4) { + animation-delay: 0.27s; + } +` diff --git a/client/src/components/trello-board/widgets/DeleteButton.jsx b/client/src/components/trello-board/widgets/DeleteButton.jsx new file mode 100644 index 000000000..a89b854b9 --- /dev/null +++ b/client/src/components/trello-board/widgets/DeleteButton.jsx @@ -0,0 +1,12 @@ +import React from "react"; +import { DelButton, DeleteWrapper } from "../styles/Elements"; + +const DeleteButton = (props) => { + return ( + + + + ); +}; + +export default DeleteButton; diff --git a/client/src/components/trello-board/widgets/EditableLabel.jsx b/client/src/components/trello-board/widgets/EditableLabel.jsx new file mode 100644 index 000000000..13564234e --- /dev/null +++ b/client/src/components/trello-board/widgets/EditableLabel.jsx @@ -0,0 +1,87 @@ +import React from 'react' +import PropTypes from 'prop-types' + +class EditableLabel extends React.Component { + constructor({value}) { + super() + this.state = {value: value} + } + + getText = el => { + return el.innerText + } + + onTextChange = ev => { + const value = this.getText(ev.target) + this.setState({value: value}) + } + + componentDidMount() { + if (this.props.autoFocus) { + this.refDiv.focus() + } + } + + onBlur = () => { + this.props.onChange(this.state.value) + } + + onPaste = ev => { + ev.preventDefault() + const value = ev.clipboardData.getData('text') + document.execCommand('insertText', false, value) + } + + getClassName = () => { + const placeholder = this.state.value === '' ? 'comPlainTextContentEditable--has-placeholder' : '' + return `comPlainTextContentEditable ${placeholder}` + } + + onKeyDown = e => { + if (e.keyCode === 13) { + this.props.onChange(this.state.value) + this.refDiv.blur() + e.preventDefault() + } + if (e.keyCode === 27) { + this.refDiv.value = this.props.value + this.setState({value: this.props.value}) + // this.refDiv.blur() + e.preventDefault() + e.stopPropagation() + } + } + + render() { + const placeholder = this.props.value.length > 0 ? false : this.props.placeholder + return ( +
(this.refDiv = ref)} + contentEditable="true" + className={this.getClassName()} + onPaste={this.onPaste} + onBlur={this.onBlur} + onInput={this.onTextChange} + onKeyDown={this.onKeyDown} + placeholder={placeholder} + /> + ) + } +} + +EditableLabel.propTypes = { + onChange: PropTypes.func, + placeholder: PropTypes.string, + autoFocus: PropTypes.bool, + inline: PropTypes.bool, + value: PropTypes.string +} + +EditableLabel.defaultProps = { + onChange: () => {}, + placeholder: '', + autoFocus: false, + inline: false, + value: '' +} +export default EditableLabel diff --git a/client/src/components/trello-board/widgets/InlineInput.jsx b/client/src/components/trello-board/widgets/InlineInput.jsx new file mode 100644 index 000000000..56c311931 --- /dev/null +++ b/client/src/components/trello-board/widgets/InlineInput.jsx @@ -0,0 +1,106 @@ +import React, { useEffect, useRef, useState } from "react"; +import PropTypes from "prop-types"; +import { InlineInput } from "../styles/Base"; +import autosize from "autosize"; + +const InlineInputController = ({ onSave, border, placeholder, value, autoFocus, resize, onCancel }) => { + const inputRef = useRef(null); + const [inputValue, setInputValue] = useState(value); + + // Effect for autosizing and initial autoFocus + useEffect(() => { + if (inputRef.current && resize !== "none") { + autosize(inputRef.current); + } + if (inputRef.current && autoFocus) { + inputRef.current.focus(); + } + }, [resize, autoFocus]); + + // Effect to update value when props change + useEffect(() => { + setInputValue(value); + }, [value]); + + const handleFocus = (e) => e.target.select(); + + const handleMouseDown = (e) => { + if (document.activeElement !== e.target) { + e.preventDefault(); + inputRef.current.focus(); + } + }; + + const handleBlur = () => { + updateValue(); + }; + + const handleKeyDown = (e) => { + if (e.keyCode === 13) { + // Enter + inputRef.current.blur(); + e.preventDefault(); + } else if (e.keyCode === 27) { + // Escape + setInputValue(value); // Reset to initial value + inputRef.current.blur(); + e.preventDefault(); + } else if (e.keyCode === 9) { + // Tab + if (inputValue.length === 0) { + onCancel(); + } + inputRef.current.blur(); + e.preventDefault(); + } + }; + + const updateValue = () => { + if (inputValue !== value) { + onSave(inputValue); + } + }; + + return ( + setInputValue(e.target.value)} + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck="false" + dataGramm="false" + rows={1} + autoFocus={autoFocus} + /> + ); +}; + +InlineInputController.propTypes = { + onSave: PropTypes.func, + onCancel: PropTypes.func, + border: PropTypes.bool, + placeholder: PropTypes.string, + value: PropTypes.string, + autoFocus: PropTypes.bool, + resize: PropTypes.oneOf(["none", "vertical", "horizontal"]) +}; + +InlineInputController.defaultProps = { + onSave: () => {}, + onCancel: () => {}, + placeholder: "", + value: "", + border: false, + autoFocus: false, + resize: "none" +}; + +export default InlineInputController; diff --git a/client/src/components/trello-board/widgets/NewLaneTitleEditor.jsx b/client/src/components/trello-board/widgets/NewLaneTitleEditor.jsx new file mode 100644 index 000000000..572046d70 --- /dev/null +++ b/client/src/components/trello-board/widgets/NewLaneTitleEditor.jsx @@ -0,0 +1,94 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { InlineInput } from "../styles/Base"; +import autosize from "autosize"; + +class NewLaneTitleEditor extends React.Component { + onKeyDown = (e) => { + if (e.keyCode === 13) { + this.refInput.blur(); + this.props.onSave(); + e.preventDefault(); + } + if (e.keyCode === 27) { + this.cancel(); + e.preventDefault(); + } + + if (e.keyCode === 9) { + if (this.getValue().length === 0) { + this.cancel(); + } else { + this.props.onSave(); + } + e.preventDefault(); + } + }; + + cancel = () => { + this.setValue(""); + this.props.onCancel(); + this.refInput.blur(); + }; + + getValue = () => this.refInput.value; + setValue = (value) => (this.refInput.value = value); + + saveValue = () => { + if (this.getValue() !== this.props.value) { + this.props.onSave(this.getValue()); + } + }; + + focus = () => this.refInput.focus(); + + setRef = (ref) => { + this.refInput = ref; + if (this.props.resize !== "none") { + autosize(this.refInput); + } + }; + + render() { + const { autoFocus, resize, border, autoResize, value, placeholder } = this.props; + + return ( + + ); + } +} + +NewLaneTitleEditor.propTypes = { + onSave: PropTypes.func, + onCancel: PropTypes.func, + border: PropTypes.bool, + placeholder: PropTypes.string, + value: PropTypes.string, + autoFocus: PropTypes.bool, + autoResize: PropTypes.bool, + resize: PropTypes.oneOf(["none", "vertical", "horizontal"]) +}; + +NewLaneTitleEditor.defaultProps = { + inputRef: () => {}, + onSave: () => {}, + onCancel: () => {}, + placeholder: "", + value: "", + border: false, + autoFocus: false, + autoResize: false, + resize: "none" +}; + +export default NewLaneTitleEditor; diff --git a/client/src/components/trello-board/widgets/index.jsx b/client/src/components/trello-board/widgets/index.jsx new file mode 100644 index 000000000..c9139bc09 --- /dev/null +++ b/client/src/components/trello-board/widgets/index.jsx @@ -0,0 +1,9 @@ +import DeleteButton from "./DeleteButton"; +import EditableLabel from "./EditableLabel"; +import InlineInput from "./InlineInput"; + +export default { + DeleteButton, + EditableLabel, + InlineInput +}; diff --git a/client/src/redux/root.reducer.js b/client/src/redux/root.reducer.js index 48113382b..ad3a1c0cb 100644 --- a/client/src/redux/root.reducer.js +++ b/client/src/redux/root.reducer.js @@ -9,6 +9,7 @@ import messagingReducer from "./messaging/messaging.reducer"; import modalsReducer from "./modals/modals.reducer"; import techReducer from "./tech/tech.reducer"; import userReducer from "./user/user.reducer"; +import trelloReducer from "./trello/trello.reducer"; // const persistConfig = { // key: "root", @@ -30,11 +31,8 @@ const rootReducer = combineReducers({ modals: modalsReducer, application: persistReducer(applicationPersistConfig, applicationReducer), tech: techReducer, - media: mediaReducer + media: mediaReducer, + trello: trelloReducer }); -export default withReduxStateSync( - // persistReducer(persistConfig, - rootReducer - //) -); +export default withReduxStateSync(rootReducer); diff --git a/client/src/redux/trello/trello.actions.js b/client/src/redux/trello/trello.actions.js new file mode 100644 index 000000000..ad0ed7990 --- /dev/null +++ b/client/src/redux/trello/trello.actions.js @@ -0,0 +1,14 @@ +import { createAction } from "redux-actions"; + +export const loadBoard = createAction("LOAD_BOARD"); +export const addLane = createAction("ADD_LANE"); +export const addCard = createAction("ADD_CARD"); +export const updateCard = createAction("UPDATE_CARD"); +export const removeCard = createAction("REMOVE_CARD"); +export const moveCardAcrossLanes = createAction("MOVE_CARD"); +export const updateCards = createAction("UPDATE_CARDS"); +export const updateLanes = createAction("UPDATE_LANES"); +export const updateLane = createAction("UPDATE_LANE"); +export const paginateLane = createAction("PAGINATE_LANE"); +export const moveLane = createAction("MOVE_LANE"); +export const removeLane = createAction("REMOVE_LANE"); diff --git a/client/src/redux/trello/trello.reducer.js b/client/src/redux/trello/trello.reducer.js new file mode 100644 index 000000000..96418ce75 --- /dev/null +++ b/client/src/redux/trello/trello.reducer.js @@ -0,0 +1,35 @@ +import Lh from "../../components/trello-board/helpers/LaneHelper"; + +const boardReducer = (state = { lanes: [] }, action) => { + const { payload, type } = action; + switch (type) { + case "LOAD_BOARD": + return Lh.initialiseLanes(state, payload); + case "ADD_CARD": + return Lh.appendCardToLane(state, payload); + case "REMOVE_CARD": + return Lh.removeCardFromLane(state, payload); + case "MOVE_CARD": + return Lh.moveCardAcrossLanes(state, payload); + case "UPDATE_CARDS": + return Lh.updateCardsForLane(state, payload); + case "UPDATE_CARD": + return Lh.updateCardForLane(state, payload); + case "UPDATE_LANES": + return Lh.updateLanes(state, payload); + case "UPDATE_LANE": + return Lh.updateLane(state, payload); + case "PAGINATE_LANE": + return Lh.paginateLane(state, payload); + case "MOVE_LANE": + return Lh.moveLane(state, payload); + case "REMOVE_LANE": + return Lh.removeLane(state, payload); + case "ADD_LANE": + return Lh.addLane(state, payload); + default: + return state; + } +}; + +export default boardReducer; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 0011cede6..2a121c51b 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -3460,6 +3460,18 @@ "validation": { "unique_vendor_name": "You must enter a unique vendor name." } - } + }, + "trello": { + "labels": { + "add_card": "Add Card", + "add_lane": "Add Lane", + "delete_lane": "Delete Lane", + "lane_actions": "Lane Actions", + "title": "Title", + "description": "Description", + "label": "Label", + "cancel": "Cancel" + } + } } } diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 463e310e6..d265b4a42 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -3460,6 +3460,18 @@ "validation": { "unique_vendor_name": "" } - } + }, + "trello": { + "labels": { + "add_card": "", + "add_lane": "", + "delete_lane": "", + "lane_actions": "", + "title": "", + "description": "", + "label": "", + "cancel": "" + } + } } } diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 8ddbeef43..f8de4a336 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -3460,6 +3460,18 @@ "validation": { "unique_vendor_name": "" } - } + }, + "trello": { + "labels": { + "add_card": "", + "add_lane": "", + "delete_lane": "", + "lane_actions": "", + "title": "", + "description": "", + "label": "", + "cancel": "" + } + } } } diff --git a/client/vite.config.js b/client/vite.config.js index 3d9f3fb7b..9208812a0 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -5,7 +5,7 @@ import * as path from "path"; import * as url from "url"; import { defineConfig } from "vite"; import { ViteEjsPlugin } from "vite-plugin-ejs"; -import eslint from 'vite-plugin-eslint'; +import eslint from "vite-plugin-eslint"; //import CompressionPlugin from 'vite-plugin-compression'; import { VitePWA } from "vite-plugin-pwa"; @@ -103,7 +103,7 @@ export default defineConfig({ }), reactVirtualized(), react(), - eslint(), + eslint() // CompressionPlugin(), //Cloudfront already compresses assets, so not needed. ], define: {