Introduce React-Trello in place of React-Kanban

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-05-09 13:22:58 -04:00
parent f77a16648f
commit f647e1ff11
49 changed files with 2632 additions and 119 deletions

View File

@@ -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!@#'

View File

@@ -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"
},

View File

@@ -11,7 +11,7 @@ module.exports = {
{
name: "Bitbucket Webhook",
script: "./webhook/index.js",
script: "./webhook/index.jsx",
env: {
NODE_ENV: "production"
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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"

View File

@@ -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",

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -19,7 +19,7 @@ const sortByParentId = (arr) => {
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.
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) => {
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) => {

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;

View 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;

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>&#10006;</GenDelButton>
</DeleteWrapper>
</LaneMenuHeader>
<LaneMenuContent>
<LaneMenuItem onClick={onDelete}>{t("trello.labels.delete_lane")}</LaneMenuItem>
</LaneMenuContent>
</Popover>
);
};
export default LaneMenu;

View 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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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
};

View 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;

View File

@@ -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);

View 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);

View 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

View 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

View 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;

View File

@@ -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)
}
})
}

View 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;

View 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;
}
`;

View 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;
`

View 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;
}
`

View File

@@ -0,0 +1,12 @@
import React from "react";
import { DelButton, DeleteWrapper } from "../styles/Elements";
const DeleteButton = (props) => {
return (
<DeleteWrapper {...props}>
<DelButton>&#10006;</DelButton>
</DeleteWrapper>
);
};
export default DeleteButton;

View 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

View 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;

View File

@@ -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;

View File

@@ -0,0 +1,9 @@
import DeleteButton from "./DeleteButton";
import EditableLabel from "./EditableLabel";
import InlineInput from "./InlineInput";
export default {
DeleteButton,
EditableLabel,
InlineInput
};

View File

@@ -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);

View 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");

View 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;

View File

@@ -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"
}
}
}
}

View File

@@ -3460,6 +3460,18 @@
"validation": {
"unique_vendor_name": ""
}
},
"trello": {
"labels": {
"add_card": "",
"add_lane": "",
"delete_lane": "",
"lane_actions": "",
"title": "",
"description": "",
"label": "",
"cancel": ""
}
}
}
}

View File

@@ -3460,6 +3460,18 @@
"validation": {
"unique_vendor_name": ""
}
},
"trello": {
"labels": {
"add_card": "",
"add_lane": "",
"delete_lane": "",
"lane_actions": "",
"title": "",
"description": "",
"label": "",
"cancel": ""
}
}
}
}

View File

@@ -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: {