Introduce React-Trello in place of React-Kanban
Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
@@ -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
|
||||
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ module.exports = {
|
||||
|
||||
{
|
||||
name: "Bitbucket Webhook",
|
||||
script: "./webhook/index.js",
|
||||
script: "./webhook/index.jsx",
|
||||
env: {
|
||||
NODE_ENV: "production"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
99
client/package-lock.json
generated
99
client/package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
className="react-kanban-card imex-kanban-card"
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor:
|
||||
cardSettings && cardSettings.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
|
||||
color: cardSettings && cardSettings.cardcolor && getContrastYIQ(bgColor)
|
||||
color: cardSettings && cardSettings.cardcolor && getContrastYIQ(bgColor),
|
||||
maxWidth: "250px",
|
||||
marginBottom: "5px"
|
||||
}}
|
||||
title={
|
||||
<Space>
|
||||
<ProductionAlert record={card} key="alert" />
|
||||
{card.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||
{card.iouparent && (
|
||||
{card.metadata.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||
{card.metadata.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<span style={{ fontWeight: "bolder" }}>
|
||||
<Link to={technician ? `/tech/joblookup?selected=${card.id}` : `/manage/jobs/${card.id}`}>
|
||||
{card.ro_number || t("general.labels.na")}
|
||||
{card.metadata.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
</span>
|
||||
</Space>
|
||||
@@ -103,7 +108,7 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett
|
||||
{cardSettings && cardSettings.ownr_nm && (
|
||||
<Col span={24}>
|
||||
{cardSettings && cardSettings.compact ? (
|
||||
<div className="ellipses">{`${card.ownr_ln || ""} ${card.ownr_co_nm || ""}`}</div>
|
||||
<div className="ellipses">{`${card.metadata.ownr_ln || ""} ${card.metadata.ownr_co_nm || ""}`}</div>
|
||||
) : (
|
||||
<div className="ellipses">
|
||||
<OwnerNameDisplay ownerObject={card} />
|
||||
@@ -112,18 +117,18 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<div className="ellipses">{`${card.v_model_yr || ""} ${
|
||||
card.v_make_desc || ""
|
||||
} ${card.v_model_desc || ""}`}</div>
|
||||
<div className="ellipses">{`${card.metadata.v_model_yr || ""} ${
|
||||
card.metadata.v_make_desc || ""
|
||||
} ${card.metadata.v_model_desc || ""}`}</div>
|
||||
</Col>
|
||||
{cardSettings && cardSettings.ins_co_nm && card.ins_co_nm && (
|
||||
{cardSettings && cardSettings.ins_co_nm && card.metadata.ins_co_nm && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<div className="ellipses">{card.ins_co_nm || ""}</div>
|
||||
<div className="ellipses">{card.metadata.ins_co_nm || ""}</div>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.clm_no && card.clm_no && (
|
||||
{cardSettings && cardSettings.clm_no && card.metadata.clm_no && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<div className="ellipses">{card.clm_no || ""}</div>
|
||||
<div className="ellipses">{card.metadata.clm_no || ""}</div>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
@@ -132,7 +137,7 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett
|
||||
<Row>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
|
||||
employee_body ? `${employee_body.first_name.substr(0, 3)} ${employee_body.last_name.charAt(0)}` : ""
|
||||
} ${card.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
|
||||
} ${card.metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`P: ${
|
||||
employee_prep ? `${employee_prep.first_name.substr(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""
|
||||
}`}</Col>
|
||||
@@ -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`}</Col>
|
||||
} ${card.metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`C: ${
|
||||
employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""
|
||||
}`}</Col>
|
||||
@@ -151,38 +156,38 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett
|
||||
<Col span={24}>
|
||||
<Row>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
|
||||
card.labhrs.aggregate.sum.mod_lb_hrs || "?"
|
||||
card.metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"
|
||||
} hrs`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
|
||||
card.larhrs.aggregate.sum.mod_lb_hrs || "?"
|
||||
card.metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"
|
||||
} hrs`}</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)} */}
|
||||
{cardSettings && cardSettings.actual_in && card.actual_in && (
|
||||
{cardSettings && cardSettings.actual_in && card.metadata.actual_in && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<Space>
|
||||
<DownloadOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{card.actual_in}</DateTimeFormatter>
|
||||
<DateTimeFormatter format="MM/DD">{card.metadata.actual_in}</DateTimeFormatter>
|
||||
</Space>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.scheduled_completion && card.scheduled_completion && (
|
||||
{cardSettings && cardSettings.scheduled_completion && card.metadata.scheduled_completion && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<Space className={pastDueAlert}>
|
||||
<CalendarOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{card.scheduled_completion}</DateTimeFormatter>
|
||||
<DateTimeFormatter format="MM/DD">{card.metadata.scheduled_completion}</DateTimeFormatter>
|
||||
</Space>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.ats && card.alt_transport && (
|
||||
{cardSettings && cardSettings.ats && card.metadata.alt_transport && (
|
||||
<Col span={12}>
|
||||
<div>{card.alt_transport || ""}</div>
|
||||
<div>{card.metadata.alt_transport || ""}</div>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.sublets && (
|
||||
<Col span={12}>
|
||||
<ProductionSubletsManageComponent subletJobLines={card.subletLines} />
|
||||
<ProductionSubletsManageComponent subletJobLines={card.metadata.subletLines} />
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.production_note && (
|
||||
@@ -192,7 +197,7 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett
|
||||
)}
|
||||
{cardSettings && cardSettings.partsstatus && (
|
||||
<Col span={24}>
|
||||
<JobPartsQueueCount parts={card.joblines_status} />
|
||||
<JobPartsQueueCount parts={card.metadata.joblines_status} />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
@@ -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 (
|
||||
<Container width={width}>
|
||||
<IndefiniteLoading loading={isMoving} />
|
||||
|
||||
<PageHeader
|
||||
title={
|
||||
<Space>
|
||||
@@ -234,18 +225,20 @@ export function ProductionBoardKanbanComponent({
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
|
||||
|
||||
<ProductionListDetailComponent jobs={data} />
|
||||
<StickyContainer>
|
||||
<Board
|
||||
style={{ height: "100%" }}
|
||||
children={boardLanes}
|
||||
disableCardDrag={isMoving}
|
||||
{...(cardSettings.stickyheader && stickyHeader)}
|
||||
renderCard={(card) => 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 })
|
||||
}}
|
||||
/>
|
||||
</StickyContainer>
|
||||
</Container>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <AddCardLink onClick={onClick}>{t("trello.labels.add_card")}</AddCardLink>;
|
||||
};
|
||||
|
||||
export default AddCardLinkComponent;
|
||||
112
client/src/components/trello-board/components/Card.jsx
Normal file
112
client/src/components/trello-board/components/Card.jsx
Normal file
@@ -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 (
|
||||
<MovableCardWrapper data-id={id} onClick={onClick} style={style} className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle draggable={cardDraggable}>
|
||||
{editable ? (
|
||||
<InlineInput
|
||||
value={title}
|
||||
border
|
||||
placeholder={t("trello.labels.title")}
|
||||
resize="vertical"
|
||||
onSave={(value) => updateCard({ title: value })}
|
||||
/>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardRightContent>
|
||||
{editable ? (
|
||||
<InlineInput
|
||||
value={label}
|
||||
border
|
||||
placeholder={t("trello.labels.label")}
|
||||
resize="vertical"
|
||||
onSave={(value) => updateCard({ label: value })}
|
||||
/>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</CardRightContent>
|
||||
{showDeleteButton && <DeleteButton onClick={handleDelete} />}
|
||||
</CardHeader>
|
||||
<Detail>
|
||||
{editable ? (
|
||||
<InlineInput
|
||||
value={description}
|
||||
border
|
||||
placeholder={t("trello.labels.description")}
|
||||
resize="vertical"
|
||||
onSave={(value) => updateCard({ description: value })}
|
||||
/>
|
||||
) : (
|
||||
description
|
||||
)}
|
||||
</Detail>
|
||||
{tags && tags.length > 0 && (
|
||||
<Footer>
|
||||
{tags.map((tag) => (
|
||||
<Tag key={tag.title} {...tag} tagStyle={tagStyle} />
|
||||
))}
|
||||
</Footer>
|
||||
)}
|
||||
</MovableCardWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
21
client/src/components/trello-board/components/Card/Tag.jsx
Normal file
21
client/src/components/trello-board/components/Card/Tag.jsx
Normal file
@@ -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 (
|
||||
<TagSpan style={style} {...otherProps}>
|
||||
{title}
|
||||
</TagSpan>
|
||||
);
|
||||
};
|
||||
|
||||
Tag.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
color: PropTypes.string,
|
||||
bgcolor: PropTypes.string,
|
||||
tagStyle: PropTypes.object
|
||||
};
|
||||
|
||||
export default Tag;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
import { LaneFooter } from "../../styles/Base";
|
||||
import { CollapseBtn, ExpandBtn } from "../../styles/Elements";
|
||||
|
||||
const LaneFooterComponent = ({ onClick, collapsed }) => (
|
||||
<LaneFooter onClick={onClick}>{collapsed ? <ExpandBtn /> : <CollapseBtn />}</LaneFooter>
|
||||
);
|
||||
|
||||
export default LaneFooterComponent;
|
||||
@@ -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 (
|
||||
<LaneHeader onDoubleClick={onDoubleClick} editLaneTitle={editLaneTitle}>
|
||||
<Title draggable={laneDraggable} style={titleStyle}>
|
||||
{editLaneTitle ? (
|
||||
<InlineInput
|
||||
value={title}
|
||||
border
|
||||
placeholder={t("trello.labels.title")}
|
||||
resize="vertical"
|
||||
onSave={updateTitle}
|
||||
/>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</Title>
|
||||
{label && (
|
||||
<RightContent>
|
||||
<span style={labelStyle}>{label}</span>
|
||||
</RightContent>
|
||||
)}
|
||||
{canAddLanes && <LaneMenu onDelete={onDelete} />}
|
||||
</LaneHeader>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -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 (
|
||||
<Popover
|
||||
position="bottom"
|
||||
PopoverContainer={CustomPopoverContainer}
|
||||
PopoverContent={CustomPopoverContent}
|
||||
trigger={<MenuButton>⋮</MenuButton>}
|
||||
>
|
||||
<LaneMenuHeader>
|
||||
<LaneMenuTitle>{t("trello.labels.lane_actions")}</LaneMenuTitle>
|
||||
<DeleteWrapper>
|
||||
<GenDelButton>✖</GenDelButton>
|
||||
</DeleteWrapper>
|
||||
</LaneMenuHeader>
|
||||
<LaneMenuContent>
|
||||
<LaneMenuItem onClick={onDelete}>{t("trello.labels.delete_lane")}</LaneMenuItem>
|
||||
</LaneMenuContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default LaneMenu;
|
||||
13
client/src/components/trello-board/components/Loader.jsx
Normal file
13
client/src/components/trello-board/components/Loader.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import {LoaderDiv, LoadingBar} from '../styles/Loader'
|
||||
|
||||
const Loader = () => (
|
||||
<LoaderDiv>
|
||||
<LoadingBar />
|
||||
<LoadingBar />
|
||||
<LoadingBar />
|
||||
<LoadingBar />
|
||||
</LoaderDiv>
|
||||
)
|
||||
|
||||
export default Loader
|
||||
@@ -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 (
|
||||
<CardForm>
|
||||
<CardWrapper>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<EditableLabel
|
||||
placeholder={t("trello.labels.title")}
|
||||
onChange={(val) => updateField("title", val)}
|
||||
autoFocus
|
||||
/>
|
||||
</CardTitle>
|
||||
<CardRightContent>
|
||||
<EditableLabel placeholder={t("trello.labels.label")} onChange={(val) => updateField("label", val)} />
|
||||
</CardRightContent>
|
||||
</CardHeader>
|
||||
<Detail>
|
||||
<EditableLabel
|
||||
placeholder={t("trello.labels.description")}
|
||||
onChange={(val) => updateField("description", val)}
|
||||
/>
|
||||
</Detail>
|
||||
</CardWrapper>
|
||||
<AddButton onClick={handleAdd}>{t("trello.labels.add_card")}</AddButton>
|
||||
<CancelButton onClick={onCancel}>{t("trello.labels.cancel")}</CancelButton>
|
||||
</CardForm>
|
||||
);
|
||||
};
|
||||
|
||||
NewCardForm.propTypes = {
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default NewCardForm;
|
||||
@@ -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 (
|
||||
<Section>
|
||||
<LaneTitle>
|
||||
<NewLaneTitleEditor
|
||||
ref={refInput}
|
||||
placeholder={t("trello.labels.title")}
|
||||
onCancel={onCancel}
|
||||
onSave={handleSubmit}
|
||||
resize="vertical"
|
||||
border
|
||||
autoFocus
|
||||
/>
|
||||
</LaneTitle>
|
||||
<NewLaneButtons>
|
||||
<AddButton onClick={handleSubmit}>{t("trello.labels.add_lane")}</AddButton>
|
||||
<CancelButton onClick={onCancel}>{t("trello.labels.cancel")}</CancelButton>
|
||||
</NewLaneButtons>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
NewLane.propTypes = {
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default NewLane;
|
||||
@@ -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 (
|
||||
<NewLaneSection>
|
||||
<AddLaneLink onClick={onClick}>{t("trello.labels.add_lane")}</AddLaneLink>
|
||||
</NewLaneSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewLaneSectionComponent;
|
||||
24
client/src/components/trello-board/components/index.js
Normal file
24
client/src/components/trello-board/components/index.js
Normal file
@@ -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
|
||||
};
|
||||
19
client/src/components/trello-board/controllers/Board.jsx
Normal file
19
client/src/components/trello-board/controllers/Board.jsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<components.GlobalStyle />
|
||||
<BoardContainer components={components} {...additionalProps} id={storeId} className={allClassNames} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Board;
|
||||
@@ -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 (
|
||||
<components.BoardWrapper style={style} {...otherProps} draggable={false}>
|
||||
<PopoverWrapper>
|
||||
<Container
|
||||
orientation="horizontal"
|
||||
onDragStart={this.onDragStart}
|
||||
dragClass={laneDragClass}
|
||||
dropClass={laneDropClass}
|
||||
onDrop={this.onLaneDrop}
|
||||
lockAxis="x"
|
||||
getChildPayload={(index) => this.getLaneDetails(index)}
|
||||
groupName={this.groupName}
|
||||
>
|
||||
{reducerData.lanes.map((lane, index) => {
|
||||
const { id, droppable, ...otherProps } = lane;
|
||||
const laneToRender = (
|
||||
<Lane
|
||||
key={id}
|
||||
boardId={this.groupName}
|
||||
components={components}
|
||||
id={id}
|
||||
getCardDetails={this.getCardDetails}
|
||||
index={index}
|
||||
droppable={droppable === undefined ? true : droppable}
|
||||
style={laneStyle || lane.style || {}}
|
||||
labelStyle={lane.labelStyle || {}}
|
||||
cardStyle={this.props.cardStyle || lane.cardStyle}
|
||||
editable={editable && !lane.disallowAddingCard}
|
||||
{...otherProps}
|
||||
{...passthroughProps}
|
||||
/>
|
||||
);
|
||||
return draggable && laneDraggable ? <Draggable key={lane.id}>{laneToRender}</Draggable> : laneToRender;
|
||||
})}
|
||||
</Container>
|
||||
</PopoverWrapper>
|
||||
{canAddLanes && (
|
||||
<Container orientation="horizontal">
|
||||
{editable && !addLaneMode ? (
|
||||
<components.NewLaneSection t={t} onClick={this.showEditableLane} />
|
||||
) : (
|
||||
addLaneMode && <components.NewLaneForm onCancel={this.hideEditableLane} onAdd={this.addNewLane} t={t} />
|
||||
)}
|
||||
</Container>
|
||||
)}
|
||||
</components.BoardWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
328
client/src/components/trello-board/controllers/Lane.jsx
Normal file
328
client/src/components/trello-board/controllers/Lane.jsx
Normal file
@@ -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 = (
|
||||
<components.Card
|
||||
key={card.id}
|
||||
index={idx}
|
||||
style={card.style || cardStyle}
|
||||
className="react-trello-card"
|
||||
onDelete={onDeleteCard}
|
||||
onClick={(e) => 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) ? (
|
||||
<Draggable key={card.id}>{cardToRender}</Draggable>
|
||||
) : (
|
||||
<span key={card.id}>{cardToRender}</span>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<components.ScrollableLane ref={this.laneDidMount} isDraggingOver={isDraggingOver}>
|
||||
<Container
|
||||
orientation="vertical"
|
||||
groupName={this.groupName}
|
||||
dragClass={cardDragClass}
|
||||
dropClass={cardDropClass}
|
||||
onDragStart={this.onDragStart}
|
||||
onDrop={(e) => 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}
|
||||
</Container>
|
||||
{editable && !addCardMode && <components.AddCardLink onClick={this.showEditableCard} laneId={id} />}
|
||||
{addCardMode && <components.NewCardForm onCancel={this.hideEditableCard} laneId={id} onAdd={this.addNewCard} />}
|
||||
</components.ScrollableLane>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<components.LaneHeader
|
||||
{...pickedProps}
|
||||
onDelete={this.removeLane}
|
||||
onDoubleClick={this.toggleLaneCollapsed}
|
||||
updateTitle={this.updateTitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<components.Section
|
||||
{...otherProps}
|
||||
key={id}
|
||||
onClick={() => onLaneClick && onLaneClick(id)}
|
||||
draggable={false}
|
||||
className={allClassNames}
|
||||
>
|
||||
{this.renderHeader({ id, cards, ...otherProps })}
|
||||
{this.renderDragContainer(isDraggingOver)}
|
||||
{loading && <components.Loader />}
|
||||
{showFooter && <components.LaneFooter onClick={this.toggleLaneCollapsed} collapsed={collapsed} />}
|
||||
</components.Section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
139
client/src/components/trello-board/dnd/Container.jsx
Normal file
139
client/src/components/trello-board/dnd/Container.jsx
Normal file
@@ -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 (
|
||||
<div style={this.props.style} ref={this.setRef}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
26
client/src/components/trello-board/dnd/Draggable.jsx
Normal file
26
client/src/components/trello-board/dnd/Draggable.jsx
Normal file
@@ -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 (
|
||||
<div {...this.props} className={`${clsName}${wrapperClass}`}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Draggable.propTypes = {
|
||||
render: PropTypes.func
|
||||
}
|
||||
|
||||
export default Draggable
|
||||
135
client/src/components/trello-board/helpers/LaneHelper.js
Normal file
135
client/src/components/trello-board/helpers/LaneHelper.js
Normal file
@@ -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;
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
38
client/src/components/trello-board/index.jsx
Normal file
38
client/src/components/trello-board/index.jsx
Normal file
@@ -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 (
|
||||
<StyleSheetManager shouldForwardProp={shouldForwardProp}>
|
||||
<Board components={{ ...DefaultComponents, ...components }} {...otherProps} />;
|
||||
</StyleSheetManager>
|
||||
);
|
||||
};
|
||||
|
||||
const shouldForwardProp = (propName, target) => {
|
||||
if (typeof target === "string") {
|
||||
return isPropValid(propName);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default TrelloBoard;
|
||||
298
client/src/components/trello-board/styles/Base.js
Normal file
298
client/src/components/trello-board/styles/Base.js
Normal file
@@ -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;
|
||||
}
|
||||
`;
|
||||
251
client/src/components/trello-board/styles/Elements.js
Normal file
251
client/src/components/trello-board/styles/Elements.js
Normal file
@@ -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;
|
||||
`
|
||||
43
client/src/components/trello-board/styles/Loader.js
Normal file
43
client/src/components/trello-board/styles/Loader.js
Normal file
@@ -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;
|
||||
}
|
||||
`
|
||||
12
client/src/components/trello-board/widgets/DeleteButton.jsx
Normal file
12
client/src/components/trello-board/widgets/DeleteButton.jsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import { DelButton, DeleteWrapper } from "../styles/Elements";
|
||||
|
||||
const DeleteButton = (props) => {
|
||||
return (
|
||||
<DeleteWrapper {...props}>
|
||||
<DelButton>✖</DelButton>
|
||||
</DeleteWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteButton;
|
||||
87
client/src/components/trello-board/widgets/EditableLabel.jsx
Normal file
87
client/src/components/trello-board/widgets/EditableLabel.jsx
Normal file
@@ -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 (
|
||||
<div
|
||||
ref={ref => (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
|
||||
106
client/src/components/trello-board/widgets/InlineInput.jsx
Normal file
106
client/src/components/trello-board/widgets/InlineInput.jsx
Normal file
@@ -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 (
|
||||
<InlineInput
|
||||
ref={inputRef}
|
||||
border={border}
|
||||
onMouseDown={handleMouseDown}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={inputValue.length === 0 ? undefined : placeholder}
|
||||
value={inputValue}
|
||||
onChange={(e) => 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;
|
||||
@@ -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 (
|
||||
<InlineInput
|
||||
style={{ resize: resize }}
|
||||
ref={this.setRef}
|
||||
border={border}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={value.length === 0 ? undefined : placeholder}
|
||||
defaultValue={value}
|
||||
rows={3}
|
||||
autoResize={autoResize}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
9
client/src/components/trello-board/widgets/index.jsx
Normal file
9
client/src/components/trello-board/widgets/index.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import DeleteButton from "./DeleteButton";
|
||||
import EditableLabel from "./EditableLabel";
|
||||
import InlineInput from "./InlineInput";
|
||||
|
||||
export default {
|
||||
DeleteButton,
|
||||
EditableLabel,
|
||||
InlineInput
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
14
client/src/redux/trello/trello.actions.js
Normal file
14
client/src/redux/trello/trello.actions.js
Normal file
@@ -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");
|
||||
35
client/src/redux/trello/trello.reducer.js
Normal file
35
client/src/redux/trello/trello.reducer.js
Normal file
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3460,6 +3460,18 @@
|
||||
"validation": {
|
||||
"unique_vendor_name": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"trello": {
|
||||
"labels": {
|
||||
"add_card": "",
|
||||
"add_lane": "",
|
||||
"delete_lane": "",
|
||||
"lane_actions": "",
|
||||
"title": "",
|
||||
"description": "",
|
||||
"label": "",
|
||||
"cancel": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3460,6 +3460,18 @@
|
||||
"validation": {
|
||||
"unique_vendor_name": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"trello": {
|
||||
"labels": {
|
||||
"add_card": "",
|
||||
"add_lane": "",
|
||||
"delete_lane": "",
|
||||
"lane_actions": "",
|
||||
"title": "",
|
||||
"description": "",
|
||||
"label": "",
|
||||
"cancel": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user