Compare commits

...

27 Commits

Author SHA1 Message Date
Dave Richer
6cf4a50a83 - Clear stage before moving to a sub-sub branch.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-06-07 15:04:36 -04:00
Dave Richer
d846894d22 Merge remote-tracking branch 'origin/master-AIO' into feature/IO-2743-Production-Board 2024-06-05 12:59:19 -04:00
Dave Richer
8f031a78c1 Merge branch 'refs/heads/master-AIO' into feature/IO-2743-Production-Board 2024-05-31 13:25:19 -04:00
Dave Richer
69b36a4c34 - quick stage clear
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-31 12:17:42 -04:00
Dave Richer
c7b8df5655 - Remove unused packages
- Consume the kafika-smooth-dnd lib as a sub dir under trello
- update above code to fix any linting errors

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-31 11:31:55 -04:00
Dave Richer
d85768b2ac - Major Performance boost reducing uncessasry board renders
- Move Orientation Toggle to Board Settings -> Display
- Delete Card Settings, replaced with Board Settings

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-30 16:36:55 -04:00
Dave Richer
a569c1f4f9 - Stability Check with test data included.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-29 16:37:55 -04:00
Dave Richer
07a8e5b216 - Missing css class
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-28 12:05:52 -04:00
Dave Richer
38bf58c613 - Missing css class
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-28 10:20:29 -04:00
Dave Richer
ba90d72d55 - Minor front end package updates
- Fixed missing key issues in JobLifecycleComponent

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-27 13:57:48 -04:00
Dave Richer
9889bee924 - Remove unused server dependencies.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-27 13:26:15 -04:00
Dave Richer
a19e4e8f16 - Server side Patch and Minor package checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-27 12:32:07 -04:00
Dave Richer
5121852fbc - Merge in Master-AIO
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-27 12:22:52 -04:00
Dave Richer
ec00697d31 - Fixed bug where Lane draggable no longer worked.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-23 16:23:47 -04:00
Dave Richer
c25714b68e - Documentation and Vertical Lane Padding
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-23 16:07:04 -04:00
Dave Richer
dc0147c5f9 - Fix Legacy bug of 'Card Settings' button, only opening, and not toggling, the card Settings
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-23 15:20:04 -04:00
Dave Richer
296afdbeee - Optimize Production Board Card Component,
- Fix issue with production note
- Refactor shared Global styles into their own global style.

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-23 15:12:49 -04:00
Dave Richer
2f8f058c5c Toggle Orientation now works dynamically (for real this time :( )
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-23 12:03:34 -04:00
Dave Richer
68784018e6 Toggle Orientation now works dynamically
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-21 17:24:34 -04:00
Dave Richer
19dfec2a34 Board Container and Lane, the last remaining class components are now functional components utilizing up to date react stuff, defaultProps deprecation fixed (rolled into function decleration)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-21 17:12:48 -04:00
Dave Richer
55d729339f Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-17 16:29:46 -04:00
Dave Richer
c3108a17f4 Checkpoint
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-17 12:58:15 -04:00
Dave Richer
d47ae64bd6 Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-16 16:41:39 -04:00
Dave Richer
095e1e9789 Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-13 19:52:32 -04:00
Dave Richer
a0b9f99dd3 Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-13 16:23:18 -04:00
Dave Richer
a33a92207b Progress Commit
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-13 13:37:19 -04:00
Dave Richer
f647e1ff11 Introduce React-Trello in place of React-Kanban
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-05-09 13:22:58 -04:00
76 changed files with 171481 additions and 17496 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!@#'
@@ -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

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,53 +0,0 @@
// craco.config.js
const TerserPlugin = require("terser-webpack-plugin");
const CracoLessPlugin = require("craco-less");
const { convertLegacyToken } = require("@ant-design/compatible/lib");
const { theme } = require("antd/lib");
const { defaultAlgorithm, defaultSeed } = theme;
const mapToken = defaultAlgorithm(defaultSeed);
const v4Token = convertLegacyToken(mapToken);
// TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { ...v4Token },
javascriptEnabled: true
}
}
}
}
],
webpack: {
configure: (webpackConfig) => {
return {
...webpackConfig,
// Required for Dev Server
devServer: {
...webpackConfig.devServer,
allowedHosts: "all"
},
optimization: {
...webpackConfig.optimization,
// Workaround for CircleCI bug caused by the number of CPUs shown
// https://github.com/facebook/create-react-app/issues/8320
minimizer: webpackConfig.optimization.minimizer.map((item) => {
if (item instanceof TerserPlugin) {
item.options.parallel = 2;
}
return item;
})
}
};
}
},
devtool: "source-map"
};

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

22214
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,90 +2,86 @@
"name": "bodyshop",
"version": "0.2.1",
"engines": {
"node": "18.18.2"
"node": ">=18.18.2"
},
"type": "module",
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/compatible": "^5.1.2",
"@ant-design/pro-layout": "^7.17.16",
"@ant-design/pro-layout": "^7.19.7",
"@apollo/client": "^3.8.10",
"@asseinfo/react-kanban": "^2.2.0",
"@fingerprintjs/fingerprintjs": "^4.2.2",
"@emotion/is-prop-valid": "^1.2.2",
"@fingerprintjs/fingerprintjs": "^4.3.0",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.1",
"@sentry/cli": "^2.28.6",
"@sentry/react": "^7.104.0",
"@splitsoftware/splitio-react": "^1.11.0",
"@reduxjs/toolkit": "^2.2.5",
"@sentry/cli": "^2.31.2",
"@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.12.0",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.2.1",
"antd": "^5.15.3",
"antd": "^5.17.4",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"axios": "^1.6.7",
"dayjs": "^1.11.10",
"autosize": "^6.0.1",
"axios": "^1.6.8",
"classnames": "^2.5.1",
"dayjs": "^1.11.11",
"dayjs-business-days2": "^1.2.2",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^10.8.1",
"firebase": "^10.12.2",
"graphql": "^16.6.0",
"i18next": "^23.10.0",
"i18next-browser-languagedetector": "^7.0.2",
"libphonenumber-js": "^1.10.57",
"logrocket": "^8.0.1",
"markerjs2": "^2.32.0",
"normalize-url": "^8.0.0",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^7.2.1",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.2",
"logrocket": "^8.1.0",
"markerjs2": "^2.32.1",
"normalize-url": "^8.0.1",
"prop-types": "^15.8.1",
"query-string": "^9.0.0",
"react": "^18.2.0",
"react-big-calendar": "^1.11.0",
"react": "^18.3.1",
"react-big-calendar": "^1.12.2",
"react-color": "^2.19.3",
"react-cookie": "^7.1.0",
"react-dom": "^18.2.0",
"react-cookie": "^7.1.4",
"react-dom": "^18.3.1",
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.0",
"react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.0.5",
"react-icons": "^5.0.1",
"react-i18next": "^14.1.2",
"react-icons": "^5.2.1",
"react-image-lightbox": "^5.1.4",
"react-joyride": "^2.7.4",
"react-joyride": "^2.8.2",
"react-markdown": "^9.0.1",
"react-number-format": "^5.3.3",
"react-number-format": "^5.3.4",
"react-popopo": "^2.1.9",
"react-product-fruits": "^2.2.6",
"react-redux": "^9.1.0",
"react-redux": "^9.1.2",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.22.2",
"react-scripts": "^5.0.1",
"react-router-dom": "^6.23.1",
"react-sticky": "^6.0.3",
"react-virtualized": "^9.22.5",
"recharts": "^2.12.2",
"recharts": "^2.12.7",
"redux": "^5.0.1",
"redux-actions": "^3.0.0",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.0",
"sass": "^1.71.1",
"socket.io-client": "^4.7.4",
"styled-components": "^6.1.8",
"sass": "^1.77.2",
"socket.io-client": "^4.7.5",
"styled-components": "^6.1.11",
"subscriptions-transport-ws": "^0.11.0",
"terser-webpack-plugin": "^5.3.10",
"userpilot": "^1.3.1",
"vite-plugin-ejs": "^1.7.0",
"web-vitals": "^3.5.2",
"workbox-core": "^7.0.0",
"workbox-expiration": "^7.0.0",
"workbox-navigation-preload": "^7.0.0",
"workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0"
"web-vitals": "^3.5.2"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "vite",
"build": "vite build",
"build": "dotenvx run --env-file=.env.development.imex -- vite build",
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
@@ -128,32 +124,30 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.23.3",
"@dotenvx/dotenvx": "^0.15.4",
"@babel/preset-react": "^7.24.6",
"@dotenvx/dotenvx": "^0.44.1",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/react": "^11.11.3",
"@sentry/webpack-plugin": "^2.14.2",
"@swc/core": "^1.3.107",
"@swc/plugin-styled-components": "^1.5.108",
"@emotion/react": "^11.11.4",
"@sentry/webpack-plugin": "^2.16.1",
"@testing-library/cypress": "^10.0.1",
"browserslist": "^4.22.3",
"browserslist-to-esbuild": "^2.1.1",
"cross-env": "^7.0.3",
"cypress": "^13.6.6",
"cypress": "^13.9.0",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.6.0",
"memfs": "^4.9.2",
"os-browserify": "^0.3.0",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^5.0.11",
"vite": "^5.2.11",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.19.0",
"vite-plugin-pwa": "^0.19.0",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.0",
"vite-plugin-style-import": "^2.0.0"
}
}

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

@@ -9,7 +9,6 @@ import axios from "axios";
const fortyFiveDaysAgo = () => dayjs().subtract(45, "day").toLocaleString();
export default function JobLifecycleDashboardComponent({ data, bodyshop, ...cardProps }) {
console.log("🚀 ~ JobLifecycleDashboardComponent ~ bodyshop:", bodyshop);
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [lifecycleData, setLifecycleData] = useState(null);
@@ -143,7 +142,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
>
<div>
{lifecycleData.summations.map((key) => (
<Tag color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
<Tag key={key.status} color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
<div
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
@@ -165,6 +164,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
size="small"
pagination={false}
columns={columns}
rowKey={(record) => record.status}
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}
/>
</Card>

View File

@@ -89,8 +89,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
console.log("Render record out today");
console.dir(record);
return record.ownerid ? (
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>

View File

@@ -12,7 +12,6 @@ import JobCloseRoGuardBills from "./job-close-ro-guard.bills";
import JobCloseRoGuardPpd from "./job-close-ro-guard.ppd";
import JobCloseRoGuardProfit from "./job-close-ro-guard.profit";
import "./job-close-ro-guard.styles.scss";
import JobCloseRoGuardSublet from "./job-close-ro-guard.sublet";
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
import InstanceRenderManager from "../../utils/instanceRenderMgr";

View File

@@ -18,6 +18,7 @@ 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";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});

View File

@@ -22,7 +22,7 @@ const CardColorLegend = ({ bodyshop }) => {
});
return (
<Col>
<Col style={{ marginLeft: "15px" }}>
<Typography>{t("production.labels.legend")}</Typography>
<List
grid={{

View File

@@ -6,7 +6,7 @@ import {
PauseCircleOutlined
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tooltip } from "antd";
import React from "react";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
@@ -18,77 +18,102 @@ import dayjs from "../../utils/day";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
/**
* Get the color of the card based on the total hours
* @param ssbuckets
* @param totalHrs
* @returns {{r: number, b: number, g: number}}
*/
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.filter((bucket) => bucket.gte <= totalHrs && (!!bucket.lt ? bucket.lt > totalHrs : true))[0];
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
let color = { r: 255, g: 255, b: 255 };
if (bucket && bucket.color) {
color = bucket.color;
if (bucket.color.rgb) {
color = bucket.color.rgb;
}
color = bucket.color.rgb || bucket.color;
}
return color;
};
function getContrastYIQ(bgColor) {
const yiq = (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000;
/**
* Get the contrast color based on the background color
* @param bgColor
* @returns {string}
*/
const getContrastYIQ = (bgColor) =>
(bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000 >= 128 ? "black" : "white";
return yiq >= 128 ? "black" : "white";
}
export default function ProductionBoardCard(technician, card, bodyshop, cardSettings) {
/**
* Production Board Card component
* @param technician
* @param card
* @param bodyshop
* @param cardSettings
* @returns {Element}
* @constructor
*/
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);
// Destructure metadata
const { metadata } = card;
if (metadata?.employee_body) {
employee_body = bodyshop.employees.find((e) => e.id === metadata.employee_body);
}
if (card.employee_prep) {
employee_prep = bodyshop.employees.find((e) => e.id === card.employee_prep);
if (metadata?.employee_prep) {
employee_prep = bodyshop.employees.find((e) => e.id === metadata.employee_prep);
}
if (card.employee_refinish) {
employee_refinish = bodyshop.employees.find((e) => e.id === card.employee_refinish);
if (metadata?.employee_refinish) {
employee_refinish = bodyshop.employees.find((e) => e.id === metadata.employee_refinish);
}
if (card.employee_csr) {
employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
if (metadata?.employee_csr) {
employee_csr = bodyshop.employees.find((e) => e.id === metadata.employee_csr);
}
// if (card.employee_csr) {
// employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
// if (metadata.?employee_csr) {
// employee_csr = bodyshop.employees.find((e) => e.id === 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"));
!!metadata?.scheduled_completion &&
((dayjs().isSameOrAfter(dayjs(metadata.scheduled_completion), "day") && "production-completion-past") ||
(dayjs().add(1, "day").isSame(dayjs(metadata.scheduled_completion), "day") && "production-completion-soon"));
const totalHrs = card.labhrs.aggregate.sum.mod_lb_hrs + card.larhrs.aggregate.sum.mod_lb_hrs;
const bgColor = cardColor(bodyshop.ssbuckets, totalHrs);
const totalHrs = useMemo(() => {
return metadata?.labhrs && metadata?.larhrs
? metadata.labhrs.aggregate.sum.mod_lb_hrs + metadata.larhrs.aggregate.sum.mod_lb_hrs
: 0;
}, [metadata]);
const bgColor = useMemo(() => cardColor(bodyshop.ssbuckets, totalHrs), [bodyshop.ssbuckets, totalHrs]);
const contrastYIQ = useMemo(() => getContrastYIQ(bgColor), [bgColor]);
return (
<Card
className="react-kanban-card imex-kanban-card"
className="react-trello-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 && contrastYIQ,
maxWidth: "250px",
margin: "5px"
}}
title={
<Space>
<ProductionAlert record={card} key="alert" />
{card.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
{card.iouparent && (
{metadata?.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
{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")}
{metadata?.ro_number || t("general.labels.na")}
</Link>
</span>
</Space>
@@ -103,7 +128,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">{`${metadata.ownr_ln || ""} ${metadata.ownr_co_nm || ""}`}</div>
) : (
<div className="ellipses">
<OwnerNameDisplay ownerObject={card} />
@@ -112,18 +137,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">{`${metadata.v_model_yr || ""} ${
metadata.v_make_desc || ""
} ${metadata.v_model_desc || ""}`}</div>
</Col>
{cardSettings && cardSettings.ins_co_nm && card.ins_co_nm && (
{cardSettings && cardSettings.ins_co_nm && metadata.ins_co_nm && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<div className="ellipses">{card.ins_co_nm || ""}</div>
<div className="ellipses">{metadata.ins_co_nm || ""}</div>
</Col>
)}
{cardSettings && cardSettings.clm_no && card.clm_no && (
{cardSettings && cardSettings.clm_no && metadata.clm_no && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<div className="ellipses">{card.clm_no || ""}</div>
<div className="ellipses">{metadata.clm_no || ""}</div>
</Col>
)}
@@ -132,7 +157,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>
} ${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 +165,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>
} ${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,48 +176,56 @@ 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 || "?"
metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"
} hrs`}</Col>
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
card.larhrs.aggregate.sum.mod_lb_hrs || "?"
metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"
} hrs`}</Col>
</Row>
</Col>
)} */}
{cardSettings && cardSettings.actual_in && card.actual_in && (
{cardSettings && cardSettings.actual_in && metadata.actual_in && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<Space>
<DownloadOutlined />
<DateTimeFormatter format="MM/DD">{card.actual_in}</DateTimeFormatter>
<DateTimeFormatter format="MM/DD">{metadata.actual_in}</DateTimeFormatter>
</Space>
</Col>
)}
{cardSettings && cardSettings.scheduled_completion && card.scheduled_completion && (
{cardSettings && cardSettings.scheduled_completion && 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">{metadata.scheduled_completion}</DateTimeFormatter>
</Space>
</Col>
)}
{cardSettings && cardSettings.ats && card.alt_transport && (
{cardSettings && cardSettings.ats && metadata.alt_transport && (
<Col span={12}>
<div>{card.alt_transport || ""}</div>
<div>{metadata.alt_transport || ""}</div>
</Col>
)}
{cardSettings && cardSettings.sublets && (
<Col span={12}>
<ProductionSubletsManageComponent subletJobLines={card.subletLines} />
<ProductionSubletsManageComponent subletJobLines={metadata.subletLines} />
</Col>
)}
{cardSettings && cardSettings.production_note && (
<Col span={24}>
{cardSettings && cardSettings.production_note && <ProductionListColumnProductionNote record={card} />}
{cardSettings && cardSettings.production_note && (
<ProductionListColumnProductionNote
record={{
production_vars: card?.metadata.production_vars,
id: card?.id,
refetch: card?.refetch
}}
/>
)}
</Col>
)}
{cardSettings && cardSettings.partsstatus && (
<Col span={24}>
<JobPartsQueueCount parts={card.joblines_status} />
<JobPartsQueueCount parts={metadata.joblines_status} />
</Col>
)}
</Row>

View File

@@ -1,125 +0,0 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Col, Form, notification, Popover, Row, Switch } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_KANBAN_SETTINGS } from "../../graphql/user.queries";
export default function ProductionBoardKanbanCardSettings({ associationSettings }) {
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
useEffect(() => {
form.setFieldsValue(associationSettings && associationSettings.kanban_settings);
}, [form, associationSettings, open]);
const { t } = useTranslation();
const handleFinish = async (values) => {
setLoading(true);
const result = await updateKbSettings({
variables: {
id: associationSettings && associationSettings.id,
ks: values
}
});
if (result.errors) {
notification.open({
type: "error",
message: t("production.errors.settings", {
error: JSON.stringify(result.errors)
})
});
}
setOpen(false);
setLoading(false);
};
const overlay = (
<div>
<Card>
<Form form={form} onFinish={handleFinish} layout="vertical">
<Row gutter={[16, 16]}>
<Col span={12}>
<Form.Item label={t("production.labels.compact")} name="compact" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.ownr_nm")} name="ownr_nm">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.clm_no")} name="clm_no">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.ins_co_nm")} name="ins_co_nm">
<Switch />
</Form.Item>
{/* <Form.Item
valuePropName="checked"
label={t("production.labels.laborhrs")}
name="laborhrs"
>
<Switch />
</Form.Item> */}
<Form.Item
valuePropName="checked"
label={t("production.labels.employeeassignments")}
name="employeeassignments"
>
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.actual_in")} name="actual_in">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.cardcolor")} name="cardcolor">
<Switch />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
valuePropName="checked"
label={t("production.labels.scheduled_completion")}
name="scheduled_completion"
>
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.ats")} name="ats">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.production_note")} name="production_note">
<Switch />
</Form.Item>
{/* <Form.Item
valuePropName='checked' label={t("production.labels.alert")} name="alert">
<Switch/>
</Form.Item> */}
<Form.Item valuePropName="checked" label={t("production.labels.sublets")} name="sublets">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.partsstatus")} name="partsstatus">
<Switch />
</Form.Item>
<Form.Item valuePropName="checked" label={t("production.labels.stickyheader")} name="stickyheader">
<Switch />
</Form.Item>
</Col>
</Row>
</Form>
<Button
onClick={() => {
form.submit();
}}
>
{t("general.actions.save")}
</Button>
</Card>
</div>
);
return (
<Popover content={overlay} open={open} placement="topRight">
<Button loading={loading} onClick={() => setOpen(true)}>
{t("production.labels.cardsettings")}
</Button>
</Popover>
);
}

View File

@@ -1,7 +1,7 @@
import { SyncOutlined } from "@ant-design/icons";
import { SyncOutlined, UnorderedListOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import Board, { moveCard } from "@asseinfo/react-kanban";
import { Button, Grid, notification, Space, Statistic } from "antd";
import Board from "../../components/trello-board/index";
import { Button, Grid, notification, Skeleton, Space, Statistic } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -19,11 +19,10 @@ import IndefiniteLoading from "../indefinite-loading/indefinite-loading.componen
import ProductionBoardFilters from "../production-board-filters/production-board-filters.component";
import ProductionBoardCard from "../production-board-kanban-card/production-board-kanban-card.component";
import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component";
import ProductionBoardKanbanCardSettings from "./production-board-kanban.card-settings.component";
//import "@asseinfo/react-kanban/dist/styles.css";
import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component";
import "./production-board-kanban.styles.scss";
import { createBoardData } from "./production-board-kanban.utils.js";
import { createBoardData, createFakeBoardData } from "./production-board-kanban.utils.js";
import ProductionBoardKanbanSettings from "./production-board-kanban.settings.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -31,7 +30,14 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
});
export function ProductionBoardKanbanComponent({
@@ -42,23 +48,30 @@ 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 });
const [loading, setLoading] = useState(true);
const [isMoving, setIsMoving] = useState(false);
const orientation = associationSettings?.kanban_settings?.orientation ? "vertical" : "horizontal";
const { t } = useTranslation();
useEffect(() => {
const boardData = createBoardData(
if (associationSettings) {
setLoading(false);
}
}, [associationSettings]);
useEffect(() => {
const boardData = createFakeBoardData(
[...bodyshop.md_ro_statuses.production_statuses, ...(bodyshop.md_ro_statuses.additional_board_statuses || [])],
data,
filter
);
boardData.columns = boardData.columns.map((d) => {
boardData.lanes = boardData.lanes.map((d) => {
return { ...d, title: `${d.title} (${d.cards.length})` };
});
setBoardLanes(boardData);
@@ -67,72 +80,75 @@ 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 movedCardWillBeLast = destinationColumn.cards.length - destination.toPosition < 1;
const sourceLane = boardLanes.lanes.find((lane) => lane.id === sourceLaneId);
const targetLane = boardLanes.lanes.find((lane) => lane.id === targetLaneId);
const lastCardInDestinationColumn = destinationColumn.cards[destinationColumn.cards.length - 1];
const movedCardWillBeFirst = position === 0;
const movedCardWillBeLast = targetLane.cards.length - position < 1;
const oldChildCard = sourceColumn.cards[source.fromPosition + 1];
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 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 update = await client.mutate({
mutation: generate_UPDATE_JOB_KANBAN(
oldChildCard ? oldChildCard.id : null,
oldChildCardNewParent,
card.id,
movedCardNewKanbanParent,
destination.toColumnId,
newChildCard ? newChildCard.id : null,
newChildCardNewParent
)
});
insertAuditTrail({
jobid: card.id,
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
type: "jobstatuschange"
});
const newChildCardNewParent = newChildCard ? cardId : null;
if (update.errors) {
try {
const update = await client.mutate({
mutation: generate_UPDATE_JOB_KANBAN(
oldChildCard ? oldChildCard.id : null,
oldChildCardNewParent,
cardId,
movedCardNewKanbanParent,
targetLaneId,
newChildCard ? newChildCard.id : null,
newChildCardNewParent
)
});
insertAuditTrail({
jobid: cardId,
operation: AuditTrailMapping.jobstatuschange(targetLaneId),
type: "jobstatuschange"
});
if (update.errors) {
notification["error"]({
message: t("production.errors.boardupdate", {
message: JSON.stringify(update.errors)
})
});
}
} catch (error) {
notification["error"]({
message: t("production.errors.boardupdate", {
message: JSON.stringify(update.errors)
message: error.message
})
});
} finally {
setIsMoving(false);
}
};
@@ -171,26 +187,21 @@ export function ProductionBoardKanbanComponent({
: standardSizes[selectedBreakpoint[0]]
: "250";
const stickyHeader = {
renderColumnHeader: ({ title }) => (
<Sticky>
{({
style,
const StickyHeader = ({ title }) => (
<Sticky>
{({ style }) => (
<div className="react-trello-column-header" style={{ ...style, zIndex: "99", backgroundColor: "#e3e3e3" }}>
<UnorderedListOutlined style={{ marginRight: "5px" }} /> {title}
</div>
)}
</Sticky>
);
// the following are also available but unused in this example
isSticky,
wasSticky,
distanceFromTop,
distanceFromBottom,
calculatedHeight
}) => (
<div className="react-kanban-column-header" style={{ ...style, zIndex: "99", backgroundColor: "#ddd" }}>
{title}
</div>
)}
</Sticky>
)
};
const NormalHeader = ({ title }) => (
<div className="react-trello-column-header" style={{ backgroundColor: "#e3e3e3" }}>
<UnorderedListOutlined style={{ marginRight: "5px" }} /> {title}
</div>
);
const cardSettings =
associationSettings &&
@@ -208,13 +219,22 @@ export function ProductionBoardKanbanComponent({
employeeassignments: true,
scheduled_completion: true,
stickyheader: false,
cardcolor: false
cardcolor: false,
orientation: false
};
const components = {
Card: (cardProps) => ProductionBoardCard({ card: cardProps, technician, bodyshop, cardSettings }),
LaneHeader: cardSettings.stickyheader && orientation === "horizontal" ? StickyHeader : NormalHeader
};
if (loading) {
return <Skeleton active />;
}
return (
<Container width={width}>
<IndefiniteLoading loading={isMoving} />
<PageHeader
title={
<Space>
@@ -230,24 +250,37 @@ export function ProductionBoardKanbanComponent({
<SyncOutlined />
</Button>
<ProductionBoardFilters filter={filter} setFilter={setFilter} loading={isMoving} />
<ProductionBoardKanbanCardSettings associationSettings={associationSettings} />
<ProductionBoardKanbanSettings parentLoading={setLoading} associationSettings={associationSettings} />
</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}
/>
</StickyContainer>
{cardSettings.stickyheader ? (
<StickyContainer>
<Board
data={boardLanes}
handleDragEnd={handleDragEnd}
style={{ height: "100%", backgroundColor: "transparent", overflowY: "auto" }}
components={components}
orientation={orientation}
collapsibleLanes
laneDraggable={false}
/>
</StickyContainer>
) : (
<div>
<Board
data={boardLanes}
handleDragEnd={handleDragEnd}
style={{ backgroundColor: "transparent", overflowY: "auto" }}
components={components}
collapsibleLanes
orientation={orientation}
laneDraggable={false}
/>
</div>
)}
</Container>
);
}
@@ -255,9 +288,9 @@ export function ProductionBoardKanbanComponent({
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardKanbanComponent);
const Container = styled.div`
.react-kanban-card-skeleton,
.react-kanban-card,
.react-kanban-card-adder-form {
.react-trello-card-skeleton,
.react-trello-card,
.react-trello-card-adder-form {
box-sizing: border-box;
max-width: ${(props) => props.width}px;
min-width: ${(props) => props.width}px;

View File

@@ -0,0 +1,243 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Col, Form, notification, Popover, Row, Checkbox, Tabs, Switch } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_KANBAN_SETTINGS } from "../../graphql/user.queries";
const { TabPane } = Tabs;
export default function ProductionBoardKanbanSettings({ associationSettings, parentLoading }) {
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
useEffect(() => {
form.setFieldsValue(associationSettings && associationSettings.kanban_settings);
}, [form, associationSettings, open]);
const { t } = useTranslation();
const handleFinish = async (values) => {
setLoading(true);
parentLoading(true);
const result = await updateKbSettings({
variables: {
id: associationSettings && associationSettings.id,
ks: values
}
});
if (result.errors) {
notification.open({
type: "error",
message: t("production.errors.settings", {
error: JSON.stringify(result.errors)
})
});
}
setOpen(false);
setLoading(false);
parentLoading(false);
setHasChanges(false);
};
const handleValuesChange = (changedValues, allValues) => {
setHasChanges(true);
};
const cardStyle = { minWidth: "50vw", marginTop: 10 };
const renderCardSettings = () => (
<>
<Card title={t("settings.sections.layout")} style={cardStyle}>
<Row gutter={[16, 16]}>
<Col span={4}>
<Form.Item name="compact" valuePropName="checked">
<Checkbox>{t("production.labels.compact")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="cardcolor" valuePropName="checked">
<Checkbox>{t("production.labels.cardcolor")}</Checkbox>
</Form.Item>
</Col>
</Row>
</Card>
<Card title={t("settings.sections.information")} style={cardStyle}>
<Row gutter={[16, 16]}>
<Col span={4}>
<Form.Item name="ownr_nm" valuePropName="checked">
<Checkbox>{t("production.labels.ownr_nm")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="clm_no" valuePropName="checked">
<Checkbox>{t("production.labels.clm_no")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="ins_co_nm" valuePropName="checked">
<Checkbox>{t("production.labels.ins_co_nm")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="employeeassignments" valuePropName="checked">
<Checkbox>{t("production.labels.employeeassignments")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="actual_in" valuePropName="checked">
<Checkbox>{t("production.labels.actual_in")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="scheduled_completion" valuePropName="checked">
<Checkbox>{t("production.labels.scheduled_completion")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="ats" valuePropName="checked">
<Checkbox>{t("production.labels.ats")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="production_note" valuePropName="checked">
<Checkbox>{t("production.labels.production_note")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="sublets" valuePropName="checked">
<Checkbox>{t("production.labels.sublets")}</Checkbox>
</Form.Item>
</Col>
<Col span={4}>
<Form.Item name="partsstatus" valuePropName="checked">
<Checkbox>{t("production.labels.partsstatus")}</Checkbox>
</Form.Item>
</Col>
</Row>
</Card>
<Card title={t("settings.sections.beta")} style={cardStyle}>
<Row gutter={[16, 16]}>
<Col span={4}>
<Form.Item name="stickyheader" valuePropName="checked">
<Checkbox>{t("production.labels.stickyheader")}</Checkbox>
</Form.Item>
</Col>
</Row>
</Card>
</>
);
const renderBoardSettings = () => (
<>
<Card title={t("settings.sections.layout")} style={cardStyle}>
<Row gutter={[16, 16]}>
<Col span={4} style={{ display: "flex", alignItems: "center" }}>
<span style={{ marginRight: "8px" }}>Orientation</span>
<Form.Item name="orientation" valuePropName="checked" style={{ marginBottom: 0 }}>
<Switch checkedChildren="Vertical" unCheckedChildren="Horizontal" defaultChecked />
</Form.Item>
</Col>
</Row>
</Card>
{/*<Card title={t("settings.sections.information")} style={cardStyle}>*/}
{/* <Row gutter={[16, 16]}>*/}
{/* <Col span={4}>*/}
{/* <Form.Item name="board_setting_3" valuePropName="checked">*/}
{/* <Checkbox>{t("board.labels.some_setting_3")}</Checkbox>*/}
{/* </Form.Item>*/}
{/* </Col>*/}
{/* <Col span={4}>*/}
{/* <Form.Item name="board_setting_4" valuePropName="checked">*/}
{/* <Checkbox>{t("board.labels.some_setting_4")}</Checkbox>*/}
{/* </Form.Item>*/}
{/* </Col>*/}
{/* </Row>*/}
{/*</Card>*/}
{/*<Card title={t("settings.sections.beta")} style={cardStyle}>*/}
{/* <Row gutter={[16, 16]}>*/}
{/* <Col span={4}>/!* Add beta settings here if any *!/</Col>*/}
{/* </Row>*/}
{/*</Card>*/}
</>
);
const renderLaneSettings = () => (
<>
<Card title={t("settings.sections.layout")} style={cardStyle}>
<Row gutter={[16, 16]}>
{/*<Col span={4}>*/}
{/* <Form.Item name="lane_setting_1" valuePropName="checked">*/}
{/* <Checkbox>{t("lane.labels.some_setting_1")}</Checkbox>*/}
{/* </Form.Item>*/}
{/*</Col>*/}
{/*<Col span={4}>*/}
{/* <Form.Item name="lane_setting_2" valuePropName="checked">*/}
{/* <Checkbox>{t("lane.labels.some_setting_2")}</Checkbox>*/}
{/* </Form.Item>*/}
{/*</Col>*/}
</Row>
</Card>
{/*<Card title={t("settings.sections.information")} style={cardStyle}>*/}
{/* <Row gutter={[16, 16]}>*/}
{/* <Col span={4}>*/}
{/* <Form.Item name="lane_setting_3" valuePropName="checked">*/}
{/* <Checkbox>{t("lane.labels.some_setting_3")}</Checkbox>*/}
{/* </Form.Item>*/}
{/* </Col>*/}
{/* <Col span={4}>*/}
{/* <Form.Item name="lane_setting_4" valuePropName="checked">*/}
{/* <Checkbox>{t("lane.labels.some_setting_4")}</Checkbox>*/}
{/* </Form.Item>*/}
{/* </Col>*/}
{/* </Row>*/}
{/*</Card>*/}
{/*<Card title={t("settings.sections.beta")} style={cardStyle}>*/}
{/* <Row gutter={[16, 16]}>*/}
{/* <Col span={4}>/!* Add beta settings here if any *!/</Col>*/}
{/* </Row>*/}
{/*</Card>*/}
</>
);
const overlay = (
<Card>
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
<Tabs defaultActiveKey="1">
<TabPane tab={t("settings.tabs.card")} key="1">
{renderCardSettings()}
</TabPane>
<TabPane tab={t("settings.tabs.board")} key="2">
{renderBoardSettings()}
</TabPane>
<TabPane tab={t("settings.tabs.lane")} key="3">
{renderLaneSettings()}
</TabPane>
</Tabs>
<Row justify="center" style={{ marginTop: 15 }} gutter={16}>
<Col span={8}>
<Button block onClick={() => setOpen(false)}>
{t("general.actions.cancel")}
</Button>
</Col>
<Col span={8}>
<Button block onClick={() => form.submit()} loading={loading} type="primary" disabled={!hasChanges}>
{t("general.actions.save")}
</Button>
</Col>
</Row>
</Form>
</Card>
);
return (
<Popover content={overlay} open={open} placement="topRight">
<Button loading={loading} onClick={() => setOpen(!open)}>
{t("settings.buttons.boardSettings")}
</Button>
</Popover>
);
}

View File

@@ -1,31 +1,31 @@
.react-kanban-board {
.react-trello-board {
padding: 5px;
}
.react-kanban-card {
.react-trello-card {
border-radius: 3px;
background-color: #fff;
padding: 4px;
margin-bottom: 7px;
}
// .react-kanban-card-skeleton,
// .react-kanban-card,
// .react-kanban-card-adder-form {
// .react-trello-card-skeleton,
// .react-trello-card,
// .react-trello-card-adder-form {
// box-sizing: border-box;
// max-width: 145px;
// min-width: 145px;
// }
.react-kanban-card--dragging {
.react-trello-card--dragging {
box-shadow: 2px 2px grey;
}
.react-kanban-card__description {
.react-trello-card__description {
padding-top: 10px;
}
.react-kanban-card__title {
.react-trello-card__title {
border-bottom: 1px solid #eee;
padding-bottom: 5px;
font-weight: bold;
@@ -33,31 +33,31 @@
justify-content: space-between;
}
.react-kanban-column {
.react-trello-column {
padding: 10px;
border-radius: 2px;
background-color: #eee;
margin: 5px;
}
.react-kanban-column input:focus {
.react-trello-column input:focus {
outline: none;
}
.react-kanban-card-adder-form {
.react-trello-card-adder-form {
border-radius: 3px;
background-color: #fff;
padding: 10px;
margin-bottom: 7px;
}
.react-kanban-card-adder-form input {
.react-trello-card-adder-form input {
border: 0px;
font-family: inherit;
font-size: inherit;
}
.react-kanban-card-adder-button {
.react-trello-card-adder-button {
width: 100%;
margin-top: 5px;
background-color: transparent;
@@ -70,11 +70,11 @@
font-weight: bold;
}
.react-kanban-card-adder-button:hover {
.react-trello-card-adder-button:hover {
background-color: #ccc;
}
.react-kanban-card-adder-form__title {
.react-trello-card-adder-form__title {
font-weight: bold;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
@@ -85,20 +85,20 @@
padding: 0px;
}
.react-kanban-card-adder-form__title:focus {
.react-trello-card-adder-form__title:focus {
outline: none;
}
.react-kanban-card-adder-form__description {
.react-trello-card-adder-form__description {
width: 100%;
margin-top: 10px;
}
.react-kanban-card-adder-form__description:focus {
.react-trello-card-adder-form__description:focus {
outline: none;
}
.react-kanban-card-adder-form__button {
.react-trello-card-adder-form__button {
background-color: #eee;
border: none;
padding: 5px;
@@ -107,39 +107,39 @@
border-radius: 3px;
}
.react-kanban-card-adder-form__button:hover {
.react-trello-card-adder-form__button:hover {
transition: 0.3s;
cursor: pointer;
background-color: #ccc;
}
.react-kanban-column-header {
.react-trello-column-header {
padding-bottom: 10px;
font-weight: bold;
}
.react-kanban-column-header input:focus {
.react-trello-column-header input:focus {
outline: none;
}
.react-kanban-column-header__button {
.react-trello-column-header__button {
color: #333333;
background-color: #ffffff;
border-color: #cccccc;
}
.react-kanban-column-header__button:hover,
.react-kanban-column-header__button:focus,
.react-kanban-column-header__button:active {
.react-trello-column-header__button:hover,
.react-trello-column-header__button:focus,
.react-trello-column-header__button:active {
background-color: #e6e6e6;
}
.react-kanban-column-adder-button {
.react-trello-column-adder-button {
border: 2px dashed #eee;
height: 132px;
margin: 5px;
}
.react-kanban-column-adder-button:hover {
.react-trello-column-adder-button:hover {
cursor: pointer;
}

View File

@@ -1,4 +1,5 @@
import { groupBy } from "lodash";
import fakeData from "./testData/board300.json";
const sortByParentId = (arr) => {
// return arr.reduce((accumulator, currentValue) => {
@@ -18,8 +19,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));
@@ -38,17 +39,19 @@ const sortByParentId = (arr) => {
return sortedList;
};
export const createFakeBoardData = () => {
return fakeData;
};
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 +78,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) => {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,6 @@ const mapDispatchToProps = (dispatch) => ({
function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
const { t } = useTranslation();
const [note, setNote] = useState((record.production_vars && record.production_vars.note) || "");
const [open, setOpen] = useState(false);

View File

@@ -9,6 +9,7 @@ export default function ProductionSubletsManageComponent({ subletJobLines }) {
const { t } = useTranslation();
const [updateJobLine] = useMutation(UPDATE_JOB_LINE_SUBLET);
const [loading, setLoading] = useState(false);
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,57 @@
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();
// TODO: Commented out because it was never called and it was causing a error
// 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,28 @@
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, StyleHorizontal, GlobalStyle, StyleVertical, ScrollableLane, Section } from "../styles/Base";
const exports = {
StyleHorizontal,
StyleVertical,
GlobalStyle,
BoardWrapper,
Loader,
ScrollableLane,
LaneHeader,
LaneFooter,
Section,
NewLaneForm,
NewLaneSection,
NewCardForm,
Card,
AddCardLink
};
export default exports;

View File

@@ -0,0 +1,28 @@
import { BoardContainer } from "../index";
import classNames from "classnames";
import { useState } from "react";
import { v1 } from "uuid";
const Board = ({ id, className, components, orientation, ...additionalProps }) => {
const [storeId] = useState(id || v1());
const allClassNames = classNames("react-trello-board", className || "");
const OrientationStyle = orientation === "horizontal" ? components.StyleHorizontal : components.StyleVertical;
return (
<>
<components.GlobalStyle />
<OrientationStyle>
<BoardContainer
components={components}
orientation={orientation}
{...additionalProps}
id={storeId}
className={allClassNames}
/>
</OrientationStyle>
</>
);
};
export default Board;

View File

@@ -0,0 +1,331 @@
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } 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";
/**
* BoardContainer is a React component that represents a Trello-like board.
* It uses Redux for state management and provides a variety of props to customize its behavior.
*
* @component
* @param {Object} props - Component props
* @param {string} props.id - The unique identifier for the board
* @param {Object} props.components - Custom components to use in the board
* @param {Object} props.data - The initial data for the board
* @param {boolean} props.draggable - Whether the board is draggable
* @param {boolean} props.laneDraggable - Whether the lanes in the board are draggable
* @param {string} props.laneDragClass - The CSS class to apply when a lane is being dragged
* @param {string} props.laneDropClass - The CSS class to apply when a lane is dropped
* @param {Object} props.style - The CSS styles to apply to the board
* @param {Function} props.onDataChange - Callback function when the board data changes
* @param {Function} props.onCardAdd - Callback function when a card is added
* @param {Function} props.onCardUpdate - Callback function when a card is updated
* @param {Function} props.onCardClick - Callback function when a card is clicked
* @param {Function} props.onBeforeCardDelete - Callback function before a card is deleted
* @param {Function} props.onCardDelete - Callback function when a card is deleted
* @param {Function} props.onLaneScroll - Callback function when a lane is scrolled
* @param {Function} props.onLaneClick - Callback function when a lane is clicked
* @param {Function} props.onLaneAdd - Callback function when a lane is added
* @param {Function} props.onLaneDelete - Callback function when a lane is deleted
* @param {Function} props.onLaneUpdate - Callback function when a lane is updated
* @param {boolean} props.editable - Whether the board is editable
* @param {boolean} props.canAddLanes - Whether lanes can be added to the board
* @param {Object} props.laneStyle - The CSS styles to apply to the lanes
* @param {Function} props.onCardMoveAcrossLanes - Callback function when a card is moved across lanes
* @param {string} props.orientation - The orientation of the board ("horizontal" or "vertical")
* @param {Function} props.eventBusHandle - Function to handle events from the event bus
* @param {Function} props.handleLaneDragStart - Callback function when a lane drag starts
* @param {Function} props.handleLaneDragEnd - Callback function when a lane drag ends
* @param {Object} props.reducerData - The initial data for the Redux reducer
* @param {Object} props.cardStyle - The CSS styles to apply to the cards
* @param {Object} props.otherProps - Any other props to pass to the board
* @returns {JSX.Element} A Trello-like board
*/
const BoardContainer = ({
id,
components,
data,
draggable = false,
laneDraggable = true,
laneDragClass = "react_trello_dragLaneClass",
laneDropClass = "react_trello_dragLaneDropClass",
style,
onDataChange = () => {},
onCardAdd = () => {},
onCardUpdate = () => {},
onCardClick = () => {},
onBeforeCardDelete = () => {},
onCardDelete = () => {},
onLaneScroll = () => {},
onLaneClick = () => {},
onLaneAdd = () => {},
onLaneDelete = () => {},
onLaneUpdate = () => {},
editable = false,
canAddLanes = false,
laneStyle,
onCardMoveAcrossLanes = () => {},
orientation = "horizontal",
eventBusHandle,
handleLaneDragStart = () => {},
handleLaneDragEnd = () => {},
reducerData,
cardStyle,
...otherProps
}) => {
const [addLaneMode, setAddLaneMode] = useState(false);
const dispatch = useDispatch();
const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
const groupName = `TrelloBoard${id}`;
const wireEventBus = useCallback(() => {
const eventBus = {
publish: (event) => {
switch (event.type) {
case "ADD_CARD":
return dispatch(actions.addCard({ laneId: event.laneId, card: event.card }));
case "REMOVE_CARD":
return dispatch(actions.removeCard({ laneId: event.laneId, cardId: event.cardId }));
case "REFRESH_BOARD":
return dispatch(actions.loadBoard(event.data));
case "MOVE_CARD":
return dispatch(
actions.moveCardAcrossLanes({
fromLaneId: event.fromLaneId,
toLaneId: event.toLaneId,
cardId: event.cardId,
index: event.index
})
);
case "UPDATE_CARDS":
return dispatch(actions.updateCards({ laneId: event.laneId, cards: event.cards }));
case "UPDATE_CARD":
return dispatch(actions.updateCard({ laneId: event.laneId, updatedCard: event.card }));
case "UPDATE_LANES":
return dispatch(actions.updateLanes(event.lanes));
case "UPDATE_LANE":
return dispatch(actions.updateLane(event.lane));
default:
return;
}
}
};
eventBusHandle(eventBus);
}, [dispatch, eventBusHandle]);
useEffect(() => {
dispatch(actions.loadBoard(data));
if (eventBusHandle) {
wireEventBus();
}
}, [data, eventBusHandle, dispatch, wireEventBus]);
useEffect(() => {
if (!isEqual(currentReducerData, reducerData)) {
onDataChange(currentReducerData);
}
}, [currentReducerData, reducerData, onDataChange]);
const onDragStart = useCallback(
({ payload }) => {
handleLaneDragStart(payload.id);
},
[handleLaneDragStart]
);
const onLaneDrop = useCallback(
({ removedIndex, addedIndex, payload }) => {
if (removedIndex !== addedIndex) {
dispatch(actions.moveLane({ oldIndex: removedIndex, newIndex: addedIndex }));
handleLaneDragEnd(removedIndex, addedIndex, payload);
}
},
[dispatch, handleLaneDragEnd]
);
const getCardDetails = useCallback(
(laneId, cardIndex) => {
return currentReducerData.lanes.find((lane) => lane.id === laneId).cards[cardIndex];
},
[currentReducerData]
);
const getLaneDetails = useCallback(
(index) => {
return currentReducerData.lanes[index];
},
[currentReducerData]
);
const hideEditableLane = () => {
setAddLaneMode(false);
};
const showEditableLane = () => {
setAddLaneMode(true);
};
const addNewLane = (params) => {
hideEditableLane();
dispatch(actions.addLane(params));
onLaneAdd(params);
};
const passThroughProps = pick(
{
id,
components,
data,
draggable,
laneDraggable,
laneDragClass,
laneDropClass,
style,
onDataChange,
onCardAdd,
onCardUpdate,
onCardClick,
onBeforeCardDelete,
onCardDelete,
onLaneScroll,
onLaneClick,
onLaneAdd,
onLaneDelete,
onLaneUpdate,
editable,
canAddLanes,
laneStyle,
onCardMoveAcrossLanes,
orientation,
eventBusHandle,
handleLaneDragStart,
handleLaneDragEnd,
reducerData,
cardStyle,
...otherProps
},
[
"onCardMoveAcrossLanes",
"onLaneScroll",
"onLaneDelete",
"onLaneUpdate",
"onCardClick",
"onBeforeCardDelete",
"onCardDelete",
"onCardAdd",
"onCardUpdate",
"onLaneClick",
"laneSortFunction",
"draggable",
"laneDraggable",
"cardDraggable",
"collapsibleLanes",
"canAddLanes",
"hideCardDeleteIcon",
"tagStyle",
"handleDragStart",
"handleDragEnd",
"cardDragClass",
"editLaneTitle",
"orientation"
]
);
return (
<components.BoardWrapper style={style} orientation={orientation} draggable={false}>
<PopoverWrapper>
<Container
orientation={orientation === "vertical" ? "vertical" : "horizontal"}
onDragStart={onDragStart}
dragClass={laneDragClass}
dropClass={laneDropClass}
onDrop={onLaneDrop}
lockAxis={orientation === "vertical" ? "y" : "x"}
getChildPayload={(index) => getLaneDetails(index)}
groupName={groupName}
>
{currentReducerData.lanes.map((lane, index) => {
const { id, droppable, ...laneOtherProps } = lane;
const laneToRender = (
<Lane
key={id}
boardId={groupName}
components={components}
id={id}
getCardDetails={getCardDetails}
index={index}
droppable={droppable === undefined ? true : droppable}
style={laneStyle || lane.style || {}}
labelStyle={lane.labelStyle || {}}
cardStyle={cardStyle || lane.cardStyle}
editable={editable && !lane.disallowAddingCard}
{...laneOtherProps}
{...passThroughProps}
/>
);
return draggable || laneDraggable ? <Draggable key={lane.id}>{laneToRender}</Draggable> : laneToRender;
})}
</Container>
</PopoverWrapper>
{canAddLanes && (
<Container orientation={orientation === "vertical" ? "vertical" : "horizontal"}>
{editable && !addLaneMode ? (
<components.NewLaneSection onClick={showEditableLane} />
) : (
addLaneMode && <components.NewLaneForm onCancel={hideEditableLane} onAdd={addNewLane} />
)}
</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,
orientation: PropTypes.string,
cardStyle: PropTypes.object
};
export default BoardContainer;

View File

@@ -0,0 +1,408 @@
import React, { useCallback, useEffect, useRef, useState } 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";
/**
* Lane is a React component that represents a lane in a Trello-like board.
* It uses Redux for state management and provides a variety of props to customize its behavior.
*
* @component
* @param {Object} props - Component props
* @param {Object} props.actions - Redux actions
* @param {string} props.id - The unique identifier for the lane
* @param {string} props.boardId - The unique identifier for the board
* @param {string} props.title - The title of the lane
* @param {number} props.index - The index of the lane
* @param {Function} props.laneSortFunction - Function to sort the cards in the lane
* @param {Object} props.style - The CSS styles to apply to the lane
* @param {Object} props.cardStyle - The CSS styles to apply to the cards
* @param {Object} props.tagStyle - The CSS styles to apply to the tags
* @param {Object} props.titleStyle - The CSS styles to apply to the title
* @param {Object} props.labelStyle - The CSS styles to apply to the label
* @param {Array} props.cards - The cards in the lane
* @param {string} props.label - The label of the lane
* @param {boolean} props.draggable - Whether the lane is draggable
* @param {boolean} props.collapsibleLanes - Whether the lanes are collapsible
* @param {boolean} props.droppable - Whether the lane is droppable
* @param {Function} props.onCardMoveAcrossLanes - Callback function when a card is moved across lanes
* @param {Function} props.onCardClick - Callback function when a card is clicked
* @param {Function} props.onBeforeCardDelete - Callback function before a card is deleted
* @param {Function} props.onCardDelete - Callback function when a card is deleted
* @param {Function} props.onCardAdd - Callback function when a card is added
* @param {Function} props.onCardUpdate - Callback function when a card is updated
* @param {Function} props.onLaneDelete - Callback function when a lane is deleted
* @param {Function} props.onLaneUpdate - Callback function when a lane is updated
* @param {Function} props.onLaneClick - Callback function when a lane is clicked
* @param {Function} props.onLaneScroll - Callback function when a lane is scrolled
* @param {boolean} props.editable - Whether the lane is editable
* @param {boolean} props.laneDraggable - Whether the lane is draggable
* @param {boolean} props.cardDraggable - Whether the cards are draggable
* @param {string} props.cardDragClass - The CSS class to apply when a card is being dragged
* @param {string} props.cardDropClass - The CSS class to apply when a card is dropped
* @param {boolean} props.canAddLanes - Whether lanes can be added to the board
* @param {boolean} props.hideCardDeleteIcon - Whether to hide the card delete icon
* @param {Object} props.components - Custom components to use in the lane
* @param {Function} props.getCardDetails - Function to get the details of a card
* @param {Function} props.handleDragStart - Callback function when a drag starts
* @param {Function} props.handleDragEnd - Callback function when a drag ends
* @param {string} props.orientation - The orientation of the lane ("horizontal" or "vertical")
* @param {string} props.className - The CSS class to apply to the lane
* @param {number} props.currentPage - The current page of the lane
* @param {Object} props.otherProps - Any other props to pass to the lane
* @returns {JSX.Element} A lane in a Trello-like board
*/
function Lane({
actions,
id,
boardId,
title,
index,
laneSortFunction,
style = {},
cardStyle = {},
tagStyle = {},
titleStyle = {},
labelStyle = {},
cards,
label,
draggable = false,
collapsibleLanes = false,
droppable = true,
onCardMoveAcrossLanes = () => {},
onCardClick = () => {},
onBeforeCardDelete = () => {},
onCardDelete = () => {},
onCardAdd = () => {},
onCardUpdate = () => {},
onLaneDelete = () => {},
onLaneUpdate = () => {},
onLaneClick = () => {},
onLaneScroll = () => {},
editable = false,
laneDraggable = false,
cardDraggable = true,
cardDragClass,
cardDropClass,
canAddLanes = false,
hideCardDeleteIcon = false,
components = {},
getCardDetails,
handleDragStart = () => {},
handleDragEnd = () => {},
orientation = "vertical",
className,
currentPage,
...otherProps
}) {
const [loading, setLoading] = useState(false);
const [currentPageFinal, setCurrentPageFinal] = useState(currentPage);
const [addCardMode, setAddCardMode] = useState(false);
const [collapsed, setCollapsed] = useState(false);
const [isDraggingOver, setIsDraggingOver] = useState(false);
const laneRef = useRef(null);
useEffect(() => {
if (!isEqual(cards, currentPageFinal)) {
setCurrentPageFinal(currentPage);
}
}, [cards, currentPage, currentPageFinal]);
const handleScroll = useCallback(
(evt) => {
const node = evt.target;
const elemScrollPosition = node.scrollHeight - node.scrollTop - node.clientHeight;
if (elemScrollPosition < 1 && onLaneScroll && !loading) {
const nextPage = currentPageFinal + 1;
setLoading(true);
onLaneScroll(nextPage, id).then((moreCards) => {
if ((moreCards || []).length > 0) {
actions.paginateLane({
laneId: id,
newCards: moreCards,
nextPage: nextPage
});
}
setLoading(false);
});
}
},
[currentPageFinal, loading, onLaneScroll, id, actions]
);
useEffect(() => {
const node = laneRef.current;
if (node) {
node.addEventListener("scroll", handleScroll);
}
return () => {
if (node) {
node.removeEventListener("scroll", handleScroll);
}
};
}, [handleScroll]);
const sortCards = (cards, sortFunction) => {
if (!cards) return [];
if (!sortFunction) return cards;
return cards.concat().sort((card1, card2) => sortFunction(card1, card2));
};
const removeCard = (cardId) => {
if (onBeforeCardDelete && typeof onBeforeCardDelete === "function") {
onBeforeCardDelete(() => {
onCardDelete && onCardDelete(cardId, id);
actions.removeCard({ laneId: id, cardId: cardId });
});
} else {
onCardDelete && onCardDelete(cardId, id);
actions.removeCard({ laneId: id, cardId: cardId });
}
};
const handleCardClick = (e, card) => {
onCardClick && onCardClick(card.id, card.metadata, card.laneId);
e.stopPropagation();
};
const showEditableCard = () => {
setAddCardMode(true);
};
const hideEditableCard = () => {
setAddCardMode(false);
};
const addNewCard = (params) => {
const laneId = id;
const newCardId = v1();
hideEditableCard();
let card = { id: newCardId, ...params };
actions.addCard({ laneId, card });
onCardAdd(card, laneId);
};
const onDragStart = ({ payload }) => {
handleDragStart && handleDragStart(payload.id, payload.laneId);
};
const shouldAcceptDrop = (sourceContainerOptions) => {
return droppable && sourceContainerOptions.groupName === groupName;
};
const onDragEnd = (laneId, result) => {
const { addedIndex, payload } = result;
if (isDraggingOver) {
setIsDraggingOver(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) {
actions.moveCardAcrossLanes({
fromLaneId: payload.laneId,
toLaneId: laneId,
cardId: payload.id,
index: addedIndex
});
onCardMoveAcrossLanes(payload.laneId, laneId, payload.id, addedIndex);
}
return response;
}
};
const updateCard = (updatedCard) => {
actions.updateCard({ laneId: id, card: updatedCard });
onCardUpdate(id, updatedCard);
};
const removeLane = () => {
actions.removeLane({ laneId: id });
onLaneDelete(id);
};
const updateTitle = (value) => {
actions.updateLane({ id, title: value });
onLaneUpdate(id, { title: value });
};
const toggleLaneCollapsed = () => {
collapsibleLanes && setCollapsed(!collapsed);
};
const groupName = `TrelloBoard${boardId}Lane`;
const renderDragContainer = (isDraggingOver) => {
const stableCards = collapsed ? [] : cards;
const cardList = sortCards(stableCards, laneSortFunction).map((card, idx) => {
const onDeleteCard = () => removeCard(card.id);
const cardToRender = (
<components.Card
key={card.id}
index={idx}
style={card.style || cardStyle}
className="react-trello-card"
onDelete={onDeleteCard}
onClick={(e) => handleCardClick(e, card)}
onChange={(updatedCard) => 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={laneRef} isDraggingOver={isDraggingOver}>
<Container
orientation={orientation === "horizontal" ? "vertical" : "horizontal"}
groupName={groupName}
dragClass={cardDragClass}
dropClass={cardDropClass}
onDragStart={onDragStart}
onDrop={(e) => onDragEnd(id, e)}
onDragEnter={() => setIsDraggingOver(true)}
onDragLeave={() => setIsDraggingOver(false)}
shouldAcceptDrop={shouldAcceptDrop}
getChildPayload={(index) => getCardDetails(id, index)}
>
{cardList}
</Container>
{editable && !addCardMode && <components.AddCardLink onClick={showEditableCard} laneId={id} />}
{addCardMode && <components.NewCardForm onCancel={hideEditableCard} laneId={id} onAdd={addNewCard} />}
</components.ScrollableLane>
);
};
const renderHeader = (pickedProps) => {
return (
<components.LaneHeader
{...pickedProps}
onDelete={removeLane}
onDoubleClick={toggleLaneCollapsed}
updateTitle={updateTitle}
/>
);
};
const allClassNames = classNames("react-trello-lane", collapsed ? "lane-collapsed" : "", className || "");
const showFooter = collapsibleLanes && cards.length > 0;
const passedProps = {
actions,
id,
boardId,
title,
index,
laneSortFunction,
style,
cardStyle,
tagStyle,
titleStyle,
labelStyle,
cards,
label,
draggable,
collapsibleLanes,
droppable,
editable,
laneDraggable,
cardDraggable,
cardDragClass,
cardDropClass,
canAddLanes,
hideCardDeleteIcon,
components,
getCardDetails,
handleDragStart,
handleDragEnd,
orientation,
className,
currentPage,
...otherProps
};
return (
<components.Section
key={id}
onClick={() => onLaneClick && onLaneClick(id)}
draggable={false}
className={allClassNames}
orientation={orientation}
{...passedProps}
>
{renderHeader({ id, cards, ...passedProps })}
{renderDragContainer(isDraggingOver)}
{loading && <components.Loader />}
{showFooter && <components.LaneFooter onClick={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,
hideCardDeleteIcon: PropTypes.bool,
components: PropTypes.object,
getCardDetails: PropTypes.func,
handleDragStart: PropTypes.func,
handleDragEnd: PropTypes.func,
orientation: PropTypes.string
};
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actions, dispatch)
});
export default connect(null, mapDispatchToProps)(Lane);

View File

@@ -0,0 +1,111 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import container, { dropHandlers } from "../smooth-dnd";
container.dropHandler = dropHandlers.reactDropHandler().handler;
container.wrapChild = (p) => p; // don't 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.prevContainer = this.containerDiv;
this.container = container(this.containerDiv, this.getContainerOptions());
}
componentWillUnmount() {
this.container.dispose();
this.container = null;
}
componentDidUpdate() {
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 = {};
const propKeys = [
"onDragStart",
"onDragEnd",
"onDrop",
"getChildPayload",
"shouldAnimateDrop",
"shouldAcceptDrop",
"onDragEnter",
"onDragLeave",
"render",
"onDropReady",
"getGhostParent"
];
propKeys.forEach((key) => {
if (this.props[key]) {
functionProps[key] = (...p) => this.props[key](...p);
}
});
return { ...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,34 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { constants } from "../smooth-dnd";
const { wrapperClass } = constants;
class Draggable extends Component {
render() {
const { render, className, children, ...restProps } = this.props;
try {
if (render) {
return React.cloneElement(render(), { className: wrapperClass });
}
const clsName = className ? `${className} ` : "";
return (
<div {...restProps} className={`${clsName}${wrapperClass}`}>
{children}
</div>
);
} catch (error) {
console.error("Error rendering Draggable component:", error);
return null; // Return null if an error occurs to prevent crashing
}
}
}
Draggable.propTypes = {
render: PropTypes.func,
className: PropTypes.string,
children: PropTypes.node
};
export default Draggable;

View File

@@ -0,0 +1,118 @@
import update from "immutability-helper";
const updateLanes = (state, lanes) => update(state, { lanes: { $set: lanes } });
const updateLaneCards = (lane, cards) => update(lane, { cards: { $set: cards } });
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 updateLanes(state, newLanes);
},
paginateLane: (state, { laneId, newCards, nextPage }) => {
const updatedLanes = LaneHelper.appendCardsToLane(state, { laneId: laneId, newCards: newCards });
updatedLanes.find((lane) => lane.id === laneId).currentPage = nextPage;
return updateLanes(state, 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) {
const cardsToUpdate =
index !== undefined
? [...lane.cards.slice(0, index), ...newCards, ...lane.cards.slice(index)]
: [...lane.cards, ...newCards];
return updateLaneCards(lane, cardsToUpdate);
} else {
return lane;
}
});
},
appendCardToLane: (state, { laneId, card, index }) => {
const newLanes = LaneHelper.appendCardsToLane(state, { laneId: laneId, newCards: [card], index });
return updateLanes(state, newLanes);
},
addLane: (state, lane) => {
const newLane = { cards: [], ...lane };
return updateLanes(state, [...state.lanes, newLane]);
},
updateLane: (state, updatedLane) => {
const newLanes = state.lanes.map((lane) => (updatedLane.id === lane.id ? { ...lane, ...updatedLane } : lane));
return updateLanes(state, newLanes);
},
removeCardFromLane: (state, { laneId, cardId }) => {
const lanes = state.lanes.map((lane) => {
if (lane.id === laneId) {
const newCards = lane.cards.filter((card) => card.id !== cardId);
return updateLaneCards(lane, newCards);
} else {
return lane;
}
});
return updateLanes(state, 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 updateLaneCards(lane, newCards);
} else {
return lane;
}
});
return LaneHelper.appendCardToLane(
{ ...state, lanes: interimLanes },
{
laneId: toLaneId,
card: cardToMove,
index: index
}
);
},
updateCardsForLane: (state, { laneId, cards }) => {
const lanes = state.lanes.map((lane) => (lane.id === laneId ? updateLaneCards(lane, cards) : lane));
return updateLanes(state, lanes);
},
updateCardForLane: (state, { laneId, card: updatedCard }) => {
const lanes = state.lanes.map((lane) => {
if (lane.id === laneId) {
const cards = lane.cards.map((card) => (card.id === updatedCard.id ? { ...card, ...updatedCard } : card));
return updateLaneCards(lane, cards);
} else {
return lane;
}
});
return updateLanes(state, 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 updateLanes(state, updatedLanes);
}
};
export default LaneHelper;

View File

@@ -0,0 +1,35 @@
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 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 }) => {
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,8 @@
import container from './src/container';
import * as constants from './src/constants';
import * as dropHandlers from './src/dropHandlers';
export default container;
export {
constants,
dropHandlers,
};

View File

@@ -0,0 +1,21 @@
export const containerInstance = 'smooth-dnd-container-instance';
export const containersInDraggable = 'smooth-dnd-containers-in-draggable';
export const defaultGroupName = '@@smooth-dnd-default-group@@';
export const wrapperClass = 'smooth-dnd-draggable-wrapper';
export const defaultGrabHandleClass = 'smooth-dnd-default-grap-handle';
export const animationClass = 'animated';
export const translationValue = '__smooth_dnd_draggable_translation_value';
export const visibilityValue = '__smooth_dnd_draggable_visibility_value';
export const ghostClass = 'smooth-dnd-ghost';
export const containerClass = 'smooth-dnd-container';
export const extraSizeForInsertion = 'smooth-dnd-extra-size-for-insertion';
export const stretcherElementClass = 'smooth-dnd-stretcher-element';
export const stretcherElementInstance = 'smooth-dnd-stretcher-instance';
export const isDraggableDetached = 'smoth-dnd-is-draggable-detached';
export const disbaleTouchActions = 'smooth-dnd-disable-touch-action';
export const noUserSelectClass = 'smooth-dnd-no-user-select';

View File

@@ -0,0 +1,61 @@
.smooth-dnd-container *{
box-sizing: border-box;
}
.smooth-dnd-disable-touch-action{
touch-action: none;
}
.smooth-dnd-container{
position: relative;
}
.smooth-dnd-container.vertical{
}
.smooth-dnd-container.horizontal{
white-space: nowrap;
}
.smooth-dnd-container.horizontal .smooth-dnd-draggable-wrapper{
height: 100%;
display: inline-block;
}
.smooth-dnd-draggable-wrapper {
overflow: hidden;
}
.smooth-dnd-draggable-wrapper.animated{
transition: transform ease;
}
.smooth-dnd-ghost {
}
.smooth-dnd-ghost *{
box-sizing: border-box;
}
.smooth-dnd-ghost.animated{
transition: all ease-in-out;
}
/* .smooth-dnd-no-user-select{
user-select: none;
}
.smooth-dnd-stretcher-element{
background-color: transparent;
}
.smooth-dnd-stretcher-element.vertical{
height: 1px;
}
.smooth-dnd-stretcher-element.horizontal{
height: 100%;
display: inline-block;
} */

View File

@@ -0,0 +1,777 @@
import Mediator from './mediator';
import layoutManager from './layoutManager';
import { hasClass, addClass, removeClass, getParent } from './utils';
import { domDropHandler } from './dropHandlers';
import {
defaultGroupName,
wrapperClass,
animationClass,
stretcherElementClass,
stretcherElementInstance,
translationValue,
containerClass,
containerInstance,
containersInDraggable
} from './constants';
const defaultOptions = {
groupName: null,
behaviour: 'move', // move | copy
orientation: 'vertical', // vertical | horizontal
getChildPayload: null,
animationDuration: 250,
autoScrollEnabled: true,
shouldAcceptDrop: null,
shouldAnimateDrop: null
};
function setAnimation(element, add, animationDuration) {
if (add) {
addClass(element, animationClass);
element.style.transitionDuration = animationDuration + 'ms';
} else {
removeClass(element, animationClass);
element.style.removeProperty('transition-duration');
}
}
function getContainer(element) {
return element ? element[containerInstance] : null;
}
function initOptions(props = defaultOptions) {
return Object.assign({}, defaultOptions, props);
}
function isDragRelevant({ element, options }) {
return function(sourceContainer, payload) {
if (options.shouldAcceptDrop) {
return options.shouldAcceptDrop(sourceContainer.getOptions(), payload);
}
const sourceOptions = sourceContainer.getOptions();
if (options.behaviour === 'copy') return false;
const parentWrapper = getParent(element, '.' + wrapperClass);
if (parentWrapper === sourceContainer.element) {
return false;
}
if (sourceContainer.element === element) return true;
if (sourceOptions.groupName && sourceOptions.groupName === options.groupName) return true;
return false;
};
}
function wrapChild(child) {
if (SmoothDnD.wrapChild) {
return SmoothDnD.wrapChild(child);
}
const div = global.document.createElement('div');
div.className = `${wrapperClass}`;
child.parentElement.insertBefore(div, child);
div.appendChild(child);
return div;
}
function wrapChildren(element) {
const draggables = [];
Array.prototype.map.call(element.children, child => {
if (child.nodeType === Node.ELEMENT_NODE) {
let wrapper = child;
if (!hasClass(child, wrapperClass)) {
wrapper = wrapChild(child);
}
wrapper[containersInDraggable] = [];
wrapper[translationValue] = 0;
draggables.push(wrapper);
} else {
if (typeof element.removeChild === "function") {
element.removeChild(child);
}
}
});
return draggables;
}
function unwrapChildren(element) {
Array.prototype.map.call(element.children, child => {
if (child.nodeType === Node.ELEMENT_NODE) {
let wrapper = child;
if (hasClass(child, wrapperClass)) {
element.insertBefore(wrapper, wrapChild.firstElementChild);
element.removeChild(wrapper);
}
}
});
}
function findDraggebleAtPos({ layout }) {
const find = (draggables, pos, startIndex, endIndex, withRespectToMiddlePoints = false) => {
if (endIndex < startIndex) {
return startIndex;
}
// binary serach draggable
if (startIndex === endIndex) {
let { begin, end } = layout.getBeginEnd(draggables[startIndex]);
// mouse pos is inside draggable
// now decide which index to return
if (pos > begin && pos <= end) {
if (withRespectToMiddlePoints) {
return pos < (end + begin) / 2 ? startIndex : startIndex + 1;
} else {
return startIndex;
}
} else {
return null;
}
} else {
const middleIndex = Math.floor((endIndex + startIndex) / 2);
const { begin, end } = layout.getBeginEnd(draggables[middleIndex]);
if (pos < begin) {
return find(draggables, pos, startIndex, middleIndex - 1, withRespectToMiddlePoints);
} else if (pos > end) {
return find(draggables, pos, middleIndex + 1, endIndex, withRespectToMiddlePoints);
} else {
if (withRespectToMiddlePoints) {
return pos < (end + begin) / 2 ? middleIndex : middleIndex + 1;
} else {
return middleIndex;
}
}
}
};
return (draggables, pos, withRespectToMiddlePoints = false) => {
return find(draggables, pos, 0, draggables.length - 1, withRespectToMiddlePoints);
};
}
function resetDraggables({ element, draggables, layout, options }) {
return function() {
draggables.forEach(p => {
setAnimation(p, false);
layout.setTranslation(p, 0);
layout.setVisibility(p, true);
p[containersInDraggable] = [];
});
if (element[stretcherElementInstance]) {
element[stretcherElementInstance].parentNode.removeChild(element[stretcherElementInstance]);
element[stretcherElementInstance] = null;
}
};
}
function setTargetContainer(draggableInfo, element, set = true) {
if (element && set) {
draggableInfo.targetElement = element;
} else {
if (draggableInfo.targetElement === element) {
draggableInfo.targetElement = null;
}
}
}
function handleDrop({ element, draggables, layout, options }) {
const draggablesReset = resetDraggables({ element, draggables, layout, options });
const dropHandler = (SmoothDnD.dropHandler || domDropHandler)({ element, draggables, layout, options });
return function(draggableInfo, { addedIndex, removedIndex }) {
draggablesReset();
// if drop zone is valid => complete drag else do nothing everything will be reverted by draggablesReset()
if (draggableInfo.targetElement || options.removeOnDropOut) {
let actualAddIndex =
addedIndex !== null ? (removedIndex !== null && removedIndex < addedIndex ? addedIndex - 1 : addedIndex) : null;
const dropHandlerParams = {
removedIndex,
addedIndex: actualAddIndex,
payload: draggableInfo.payload,
droppedElement: draggableInfo.element.firstElementChild
};
dropHandler(dropHandlerParams, options.onDrop);
}
};
}
function getContainerProps(element, initialOptions) {
const options = initOptions(initialOptions);
const draggables = wrapChildren(element, options.orientation, options.animationDuration);
// set flex classes before layout is inited for scroll listener
addClass(element, `${containerClass} ${options.orientation}`);
const layout = layoutManager(element, options.orientation, options.animationDuration);
return {
element,
draggables,
options,
layout
};
}
function getRelaventParentContainer(container, relevantContainers) {
let current = container.element;
while (current) {
const containerOfParentElement = getContainer(current.parentElement);
if (containerOfParentElement && relevantContainers.indexOf(containerOfParentElement) > -1) {
return {
container: containerOfParentElement,
draggable: current
};
}
current = current.parentElement;
}
return null;
}
function registerToParentContainer(container, relevantContainers) {
const parentInfo = getRelaventParentContainer(container, relevantContainers);
if (parentInfo) {
parentInfo.container.getChildContainers().push(container);
container.setParentContainer(parentInfo.container);
//current should be draggable
parentInfo.draggable[containersInDraggable].push(container);
}
}
function getRemovedItem({ draggables, element, options }) {
let prevRemovedIndex = null;
return ({ draggableInfo, dragResult }) => {
let removedIndex = prevRemovedIndex;
if (prevRemovedIndex == null && draggableInfo.container.element === element && options.behaviour !== 'copy') {
removedIndex = prevRemovedIndex = draggableInfo.elementIndex;
}
return { removedIndex };
};
}
function setRemovedItemVisibilty({ draggables, layout }) {
return ({ draggableInfo, dragResult }) => {
if (dragResult.removedIndex !== null) {
layout.setVisibility(draggables[dragResult.removedIndex], false);
}
};
}
function getPosition({ element, layout }) {
return ({ draggableInfo }) => {
return {
pos: !getContainer(element).isPosInChildContainer() ? layout.getPosition(draggableInfo.position) : null
};
};
}
function notifyParentOnPositionCapture({ element }) {
let isCaptured = false;
return ({ draggableInfo, dragResult }) => {
if (getContainer(element).getParentContainer() && isCaptured !== (dragResult.pos !== null)) {
isCaptured = dragResult.pos !== null;
getContainer(element)
.getParentContainer()
.onChildPositionCaptured(isCaptured);
}
};
}
function getElementSize({ layout }) {
let elementSize = null;
return ({ draggableInfo, dragResult }) => {
if (dragResult.pos === null) {
return (elementSize = null);
} else {
elementSize = elementSize || layout.getSize(draggableInfo.element);
}
return { elementSize };
};
}
function handleTargetContainer({ element }) {
return ({ draggableInfo, dragResult }) => {
setTargetContainer(draggableInfo, element, !!dragResult.pos);
};
}
function getDragInsertionIndex({ draggables, layout }) {
const findDraggable = findDraggebleAtPos({ layout });
return ({ dragResult: { shadowBeginEnd, pos } }) => {
if (!shadowBeginEnd) {
const index = findDraggable(draggables, pos, true);
return index !== null ? index : draggables.length;
} else {
if (shadowBeginEnd.begin + shadowBeginEnd.beginAdjustment <= pos && shadowBeginEnd.end >= pos) {
// position inside ghost
return null;
}
}
if (pos < shadowBeginEnd.begin + shadowBeginEnd.beginAdjustment) {
return findDraggable(draggables, pos);
} else if (pos > shadowBeginEnd.end) {
return findDraggable(draggables, pos) + 1;
} else {
return draggables.length;
}
};
}
function getDragInsertionIndexForDropZone({ draggables, layout }) {
return ({ dragResult: { pos } }) => {
return pos !== null ? { addedIndex: 0 } : { addedIndex: null };
};
}
function getShadowBeginEndForDropZone({ draggables, layout }) {
let prevAddedIndex = null;
return ({ dragResult: { addedIndex } }) => {
if (addedIndex !== prevAddedIndex) {
prevAddedIndex = addedIndex;
const { begin, end } = layout.getBeginEndOfContainer();
return {
shadowBeginEnd: {
rect: layout.getTopLeftOfElementBegin(begin, end)
}
};
}
};
}
function invalidateShadowBeginEndIfNeeded(params) {
const shadowBoundsGetter = getShadowBeginEnd(params);
return ({ draggableInfo, dragResult }) => {
if (draggableInfo.invalidateShadow) {
return shadowBoundsGetter({ draggableInfo, dragResult });
}
return null;
};
}
function getNextAddedIndex(params) {
const getIndexForPos = getDragInsertionIndex(params);
return ({ dragResult }) => {
let index = null;
if (dragResult.pos !== null) {
index = getIndexForPos({ dragResult });
if (index === null) {
index = dragResult.addedIndex;
}
}
return {
addedIndex: index
};
};
}
function resetShadowAdjustment() {
let lastAddedIndex = null;
return ({ dragResult: { addedIndex, shadowBeginEnd } }) => {
if (addedIndex !== lastAddedIndex && lastAddedIndex !== null && shadowBeginEnd) {
shadowBeginEnd.beginAdjustment = 0;
}
lastAddedIndex = addedIndex;
};
}
function handleInsertionSizeChange({ element, draggables, layout, options }) {
let strectherElement = null;
return function({ dragResult: { addedIndex, removedIndex, elementSize } }) {
if (removedIndex === null) {
if (addedIndex !== null) {
if (!strectherElement) {
const containerBeginEnd = layout.getBeginEndOfContainer();
containerBeginEnd.end = containerBeginEnd.begin + layout.getSize(element);
const hasScrollBar = layout.getScrollSize(element) > layout.getSize(element);
const containerEnd = hasScrollBar
? containerBeginEnd.begin + layout.getScrollSize(element) - layout.getScrollValue(element)
: containerBeginEnd.end;
const lastDraggableEnd =
draggables.length > 0
? layout.getBeginEnd(draggables[draggables.length - 1]).end -
draggables[draggables.length - 1][translationValue]
: containerBeginEnd.begin;
if (lastDraggableEnd + elementSize > containerEnd) {
strectherElement = global.document.createElement('div');
strectherElement.className = stretcherElementClass + ' ' + options.orientation;
const stretcherSize = elementSize + lastDraggableEnd - containerEnd;
layout.setSize(strectherElement.style, `${stretcherSize}px`);
element.appendChild(strectherElement);
element[stretcherElementInstance] = strectherElement;
return {
containerBoxChanged: true
};
}
}
} else {
if (strectherElement) {
layout.setTranslation(strectherElement, 0);
let toRemove = strectherElement;
strectherElement = null;
element.removeChild(toRemove);
element[stretcherElementInstance] = null;
return {
containerBoxChanged: true
};
}
}
}
};
}
function calculateTranslations({ element, draggables, layout }) {
let prevAddedIndex = null;
let prevRemovedIndex = null;
return function({ dragResult: { addedIndex, removedIndex, elementSize } }) {
if (addedIndex !== prevAddedIndex || removedIndex !== prevRemovedIndex) {
for (let index = 0; index < draggables.length; index++) {
if (index !== removedIndex) {
const draggable = draggables[index];
let translate = 0;
if (removedIndex !== null && removedIndex < index) {
translate -= layout.getSize(draggables[removedIndex]);
}
if (addedIndex !== null && addedIndex <= index) {
translate += elementSize;
}
layout.setTranslation(draggable, translate);
}
}
prevAddedIndex = addedIndex;
prevRemovedIndex = removedIndex;
return { addedIndex, removedIndex };
}
};
}
function getShadowBeginEnd({ draggables, layout }) {
let prevAddedIndex = null;
return ({ draggableInfo, dragResult }) => {
const { addedIndex, removedIndex, elementSize, pos, shadowBeginEnd } = dragResult;
if (pos !== null) {
if (addedIndex !== null && (draggableInfo.invalidateShadow || addedIndex !== prevAddedIndex)) {
if (prevAddedIndex) prevAddedIndex = addedIndex;
let beforeIndex = addedIndex - 1;
let begin = 0;
let afterBounds = null;
let beforeBounds = null;
if (beforeIndex === removedIndex) {
beforeIndex--;
}
if (beforeIndex > -1) {
const beforeSize = layout.getSize(draggables[beforeIndex]);
beforeBounds = layout.getBeginEnd(draggables[beforeIndex]);
if (elementSize < beforeSize) {
const threshold = (beforeSize - elementSize) / 2;
begin = beforeBounds.end - threshold;
} else {
begin = beforeBounds.end;
}
} else {
beforeBounds = { end: layout.getBeginEndOfContainer().begin };
}
let end = 10000;
let afterIndex = addedIndex;
if (afterIndex === removedIndex) {
afterIndex++;
}
if (afterIndex < draggables.length) {
const afterSize = layout.getSize(draggables[afterIndex]);
afterBounds = layout.getBeginEnd(draggables[afterIndex]);
if (elementSize < afterSize) {
const threshold = (afterSize - elementSize) / 2;
end = afterBounds.begin + threshold;
} else {
end = afterBounds.begin;
}
} else {
afterBounds = { begin: layout.getContainerRectangles().end };
}
const shadowRectTopLeft =
beforeBounds && afterBounds ? layout.getTopLeftOfElementBegin(beforeBounds.end, afterBounds.begin) : null;
return {
shadowBeginEnd: {
begin,
end,
rect: shadowRectTopLeft,
beginAdjustment: shadowBeginEnd ? shadowBeginEnd.beginAdjustment : 0
}
};
} else {
return null;
}
} else {
prevAddedIndex = null;
return {
shadowBeginEnd: null
};
}
};
}
function handleFirstInsertShadowAdjustment() {
let lastAddedIndex = null;
return ({ dragResult: { pos, addedIndex, shadowBeginEnd }, draggableInfo: { invalidateShadow } }) => {
if (pos !== null) {
if (addedIndex != null && lastAddedIndex === null) {
if (pos < shadowBeginEnd.begin) {
const beginAdjustment = pos - shadowBeginEnd.begin - 5;
shadowBeginEnd.beginAdjustment = beginAdjustment;
}
lastAddedIndex = addedIndex;
}
} else {
lastAddedIndex = null;
}
};
}
function fireDragEnterLeaveEvents({ options }) {
let wasDragIn = false;
return ({ dragResult: { pos } }) => {
const isDragIn = !!pos;
if (isDragIn !== wasDragIn) {
wasDragIn = isDragIn;
if (isDragIn) {
options.onDragEnter && options.onDragEnter();
} else {
options.onDragLeave && options.onDragLeave();
return {
dragLeft: true
};
}
}
};
}
function fireOnDropReady({ options }) {
let lastAddedIndex = null;
return ({ dragResult: { addedIndex, removedIndex }, draggableInfo: { payload, element } }) => {
if (options.onDropReady && lastAddedIndex !== addedIndex) {
lastAddedIndex = addedIndex;
let adjustedAddedIndex = addedIndex;
if (removedIndex !== null && addedIndex > removedIndex) {
adjustedAddedIndex--;
}
options.onDropReady({ addedIndex: adjustedAddedIndex, removedIndex, payload, element: element.firstElementChild });
}
}
}
function getDragHandler(params) {
if (params.options.behaviour === 'drop-zone') {
// sorting is disabled in container, addedIndex will always be 0 if dropped in
return compose(params)(
getRemovedItem,
setRemovedItemVisibilty,
getPosition,
notifyParentOnPositionCapture,
getElementSize,
handleTargetContainer,
getDragInsertionIndexForDropZone,
getShadowBeginEndForDropZone,
fireDragEnterLeaveEvents,
fireOnDropReady
);
} else {
return compose(params)(
getRemovedItem,
setRemovedItemVisibilty,
getPosition,
notifyParentOnPositionCapture,
getElementSize,
handleTargetContainer,
invalidateShadowBeginEndIfNeeded,
getNextAddedIndex,
resetShadowAdjustment,
handleInsertionSizeChange,
calculateTranslations,
getShadowBeginEnd,
handleFirstInsertShadowAdjustment,
fireDragEnterLeaveEvents,
fireOnDropReady
);
}
}
function getDefaultDragResult() {
return {
addedIndex: null,
removedIndex: null,
elementSize: null,
pos: null,
shadowBeginEnd: null
};
}
function compose(params) {
return (...functions) => {
const hydratedFunctions = functions.map(p => p(params));
let result = null;
return draggableInfo => {
result = hydratedFunctions.reduce((dragResult, fn) => {
return Object.assign(dragResult, fn({ draggableInfo, dragResult }));
}, result || getDefaultDragResult());
return result;
};
};
}
// Container definition begin
function Container(element) {
return function(options) {
let dragResult = null;
let lastDraggableInfo = null;
const props = getContainerProps(element, options);
let dragHandler = getDragHandler(props);
let dropHandler = handleDrop(props);
let parentContainer = null;
let posIsInChildContainer = false;
let childContainers = [];
function processLastDraggableInfo() {
if (lastDraggableInfo !== null) {
lastDraggableInfo.invalidateShadow = true;
dragResult = dragHandler(lastDraggableInfo);
lastDraggableInfo.invalidateShadow = false;
}
}
function onChildPositionCaptured(isCaptured) {
posIsInChildContainer = isCaptured;
if (parentContainer) {
parentContainer.onChildPositionCaptured(isCaptured);
if (lastDraggableInfo) {
dragResult = dragHandler(lastDraggableInfo);
}
}
}
function setDraggables(draggables, element, options) {
const newDraggables = wrapChildren(element, options.orientation, options.animationDuration);
for (let i = 0; i < newDraggables.length; i++) {
draggables[i] = newDraggables[i];
}
for (let i = 0; i < draggables.length - newDraggables.length; i++) {
draggables.pop();
}
}
function prepareDrag(container, relevantContainers) {
const element = container.element;
const draggables = props.draggables;
const options = container.getOptions();
setDraggables(draggables, element, options);
container.layout.invalidateRects();
registerToParentContainer(container, relevantContainers);
draggables.forEach(p => setAnimation(p, true, options.animationDuration));
}
props.layout.setScrollListener(function() {
processLastDraggableInfo();
});
function handleDragLeftDeferedTranslation() {
if (dragResult.dragLeft && props.options.behaviour !== 'drop-zone') {
dragResult.dragLeft = false;
setTimeout(() => {
if (dragResult) calculateTranslations(props)({ dragResult });
}, 20);
}
}
function dispose(container) {
unwrapChildren(container.element);
}
return {
element,
draggables: props.draggables,
isDragRelevant: isDragRelevant(props),
getScale: props.layout.getContainerScale,
layout: props.layout,
getChildContainers: () => childContainers,
onChildPositionCaptured,
dispose,
prepareDrag,
isPosInChildContainer: () => posIsInChildContainer,
handleDrag: function(draggableInfo) {
lastDraggableInfo = draggableInfo;
dragResult = dragHandler(draggableInfo);
handleDragLeftDeferedTranslation();
return dragResult;
},
handleDrop: function(draggableInfo) {
lastDraggableInfo = null;
onChildPositionCaptured(false);
dragHandler = getDragHandler(props);
dropHandler(draggableInfo, dragResult);
dragResult = null;
parentContainer = null;
childContainers = [];
},
getDragResult: function() {
return dragResult;
},
getTranslateCalculator: function(...params) {
return calculateTranslations(props)(...params);
},
setParentContainer: e => {
parentContainer = e;
},
getParentContainer: () => parentContainer,
onTranslated: () => {
processLastDraggableInfo();
},
getOptions: () => props.options,
setDraggables: () => {
setDraggables(props.draggables, element, props.options);
}
};
};
}
const options = {
behaviour: 'move',
groupName: 'bla bla', // if not defined => container will not interfere with other containers
orientation: 'vertical',
dragHandleSelector: null,
nonDragAreaSelector: 'some selector',
dragBeginDelay: 0,
animationDuration: 180,
autoScrollEnabled: true,
lockAxis: true,
dragClass: null,
dropClass: null,
onDragStart: (index, payload) => {},
onDrop: ({ removedIndex, addedIndex, payload, element }) => {},
getChildPayload: index => null,
shouldAnimateDrop: (sourceContainerOptions, payload) => true,
shouldAcceptDrop: (sourceContainerOptions, payload) => true,
onDragEnter: () => {},
onDragLeave: () => { },
onDropReady: ({ removedIndex, addedIndex, payload, element }) => { },
};
// exported part of container
function SmoothDnD(element, options) {
const containerIniter = Container(element);
const container = containerIniter(options);
element[containerInstance] = container;
Mediator.register(container);
return {
dispose: function() {
Mediator.unregister(container);
container.layout.dispose();
container.dispose(container);
}
};
}
export default SmoothDnD;

View File

@@ -0,0 +1,208 @@
import { getScrollingAxis, getVisibleRect } from "./utils";
const maxSpeed = 1500; // px/s
// const minSpeed = 20; // px/s
function addScrollValue(element, axis, value) {
if (element) {
if (element !== window) {
if (axis === "x") {
element.scrollLeft += value;
} else {
element.scrollTop += value;
}
} else {
if (axis === "x") {
element.scrollBy(value, 0);
} else {
element.scrollBy(0, value);
}
}
}
}
const createAnimator = (element, axis = "y") => {
let isAnimating = false;
let request = null;
let startTime = null;
let direction = null;
let speed = null;
function animate(_direction, _speed) {
direction = _direction;
speed = _speed;
isAnimating = true;
if (isAnimating) {
start();
}
}
function start() {
if (request === null) {
request = requestAnimationFrame((timestamp) => {
if (startTime === null) {
startTime = timestamp;
}
const timeDiff = timestamp - startTime;
startTime = timestamp;
let distanceDiff = (timeDiff / 1000) * speed;
distanceDiff = direction === "begin" ? 0 - distanceDiff : distanceDiff;
addScrollValue(element, axis, distanceDiff);
request = null;
start();
});
}
}
function stop() {
if (isAnimating) {
cancelAnimationFrame(request);
isAnimating = false;
startTime = null;
request = null;
}
}
return {
animate,
stop
};
};
function getAutoScrollInfo(position, scrollableInfo) {
const { left, right, top, bottom } = scrollableInfo.rect;
const { x, y } = position;
if (x < left || x > right || y < top || y > bottom) {
return null;
}
let begin;
let end;
let pos;
if (scrollableInfo.axis === "x") {
begin = left;
end = right;
pos = x;
} else {
begin = top;
end = bottom;
pos = y;
}
const moveDistance = 100;
if (end - pos < moveDistance) {
return {
direction: "end",
speedFactor: (moveDistance - (end - pos)) / moveDistance
};
} else if (pos - begin < moveDistance) {
// console.log(pos - begin);
return {
direction: "begin",
speedFactor: (moveDistance - (pos - begin)) / moveDistance
};
}
}
function scrollableInfo(element) {
const result = {
element,
rect: getVisibleRect(element, element.getBoundingClientRect()),
descendants: [],
invalidate,
axis: null,
dispose
};
function dispose() {
element.removeEventListener("scroll", invalidate);
}
function invalidate() {
result.rect = getVisibleRect(element, element.getBoundingClientRect());
result.descendants.forEach((p) => p.invalidate());
}
element.addEventListener("scroll", invalidate);
return result;
}
function handleCurrentElement(current, scrollables, firstDescendentScrollable) {
const scrollingAxis = getScrollingAxis(current);
if (scrollingAxis) {
if (!scrollables.some((p) => p.element === current)) {
const info = scrollableInfo(current);
if (firstDescendentScrollable) {
info.descendants.push(firstDescendentScrollable);
}
firstDescendentScrollable = info;
if (scrollingAxis === "xy") {
scrollables.push(Object.assign({}, info, { axis: "x" }));
scrollables.push(Object.assign({}, info, { axis: "y" }, { descendants: [] }));
} else {
scrollables.push(Object.assign({}, info, { axis: scrollingAxis }));
}
}
}
return { current: current.parentElement, firstDescendentScrollable };
}
function getScrollableElements(containerElements) {
const scrollables = [];
let firstDescendentScrollable = null;
containerElements.forEach((el) => {
let current = el;
firstDescendentScrollable = null;
while (current) {
const result = handleCurrentElement(current, scrollables, firstDescendentScrollable);
current = result.current;
firstDescendentScrollable = result.firstDescendentScrollable;
}
});
return scrollables;
}
function getScrollableAnimator(scrollableInfo) {
return Object.assign(scrollableInfo, createAnimator(scrollableInfo.element, scrollableInfo.axis));
}
function getWindowAnimators() {
function getWindowRect() {
return {
left: 0,
right: global.innerWidth,
top: 0,
bottom: global.innerHeight
};
}
return [
Object.assign({ rect: getWindowRect(), axis: "y" }, createAnimator(global)),
Object.assign({ rect: getWindowRect(), axis: "x" }, createAnimator(global, "x"))
];
}
const dragScroller = (containers) => {
const scrollablesInfo = getScrollableElements(containers.map((p) => p.element));
const animators = [...scrollablesInfo.map(getScrollableAnimator), ...getWindowAnimators()];
return ({ draggableInfo, reset }) => {
if (animators.length) {
if (reset) {
animators.forEach((p) => p.stop());
scrollablesInfo.forEach((p) => p.dispose());
return null;
}
animators.forEach((animator) => {
const scrollParams = getAutoScrollInfo(draggableInfo.mousePosition, animator);
if (scrollParams) {
animator.animate(scrollParams.direction, scrollParams.speedFactor * maxSpeed);
} else {
animator.stop();
}
});
}
};
};
export default dragScroller;

View File

@@ -0,0 +1,49 @@
import { addChildAt, removeChildAt } from './utils';
import {
wrapperClass,
animationClass,
containersInDraggable
} from './constants';
export function domDropHandler({ element, draggables, layout, options }) {
return (dropResult, onDrop) => {
const { removedIndex, addedIndex, droppedElement } = dropResult;
let removedWrapper = null;
if (removedIndex !== null) {
removedWrapper = removeChildAt(element, removedIndex);
draggables.splice(removedIndex, 1);
}
if (addedIndex !== null) {
const wrapper = global.document.createElement('div');
wrapper.className = `${wrapperClass}`;
wrapper.appendChild(removedWrapper && removedWrapper.firstElementChild ? removedWrapper.firstElementChild : droppedElement);
wrapper[containersInDraggable] = [];
addChildAt(element, wrapper, addedIndex);
if (addedIndex >= draggables.length) {
draggables.push(wrapper);
} else {
draggables.splice(addedIndex, 0, wrapper);
}
}
if (onDrop) {
onDrop(dropResult);
}
};
}
export function reactDropHandler() {
const handler = ({ element, draggables, layout, options }) => {
return (dropResult, onDrop) => {
if (onDrop) {
onDrop(dropResult);
}
};
};
return {
handler
};
}

View File

@@ -0,0 +1,288 @@
import * as Utils from './utils';
import { translationValue, visibilityValue, extraSizeForInsertion, containersInDraggable } from './constants';
const horizontalMap = {
size: 'offsetWidth',
distanceToParent: 'offsetLeft',
translate: 'transform',
begin: 'left',
end: 'right',
dragPosition: 'x',
scrollSize: 'scrollWidth',
offsetSize: 'offsetWidth',
scrollValue: 'scrollLeft',
scale: 'scaleX',
setSize: 'width',
setters: {
'translate': (val) => `translate3d(${val}px, 0, 0)`
}
};
const verticalMap = {
size: 'offsetHeight',
distanceToParent: 'offsetTop',
translate: 'transform',
begin: 'top',
end: 'bottom',
dragPosition: 'y',
scrollSize: 'scrollHeight',
offsetSize: 'offsetHeight',
scrollValue: 'scrollTop',
scale: 'scaleY',
setSize: 'height',
setters: {
'translate': (val) => `translate3d(0,${val}px, 0)`
}
};
function orientationDependentProps(map) {
function get(obj, prop) {
const mappedProp = map[prop];
return obj[mappedProp || prop];
}
function set(obj, prop, value) {
requestAnimationFrame(() => {
obj[map[prop]] = map.setters[prop] ? map.setters[prop](value) : value;
});
}
return { get, set };
}
export default function layoutManager(containerElement, orientation, _animationDuration) {
containerElement[extraSizeForInsertion] = 0;
const animationDuration = _animationDuration;
const map = orientation === 'horizontal' ? horizontalMap : verticalMap;
const propMapper = orientationDependentProps(map);
const values = {
translation: 0
};
let registeredScrollListener = null;
global.addEventListener('resize', function() {
invalidateContainerRectangles(containerElement);
// invalidateContainerScale(containerElement);
});
setTimeout(() => {
invalidate();
}, 10);
// invalidate();
const scrollListener = Utils.listenScrollParent(containerElement, function() {
invalidateContainerRectangles(containerElement);
registeredScrollListener && registeredScrollListener();
});
function invalidate() {
invalidateContainerRectangles(containerElement);
invalidateContainerScale(containerElement);
}
let visibleRect;
function invalidateContainerRectangles(containerElement) {
values.rect = Utils.getContainerRect(containerElement);
values.visibleRect = Utils.getVisibleRect(containerElement, values.rect);
}
function invalidateContainerScale(containerElement) {
const rect = containerElement.getBoundingClientRect();
values.scaleX = containerElement.offsetWidth ? ((rect.right - rect.left) / containerElement.offsetWidth) : 1;
values.scaleY = containerElement.offsetHeight ? ((rect.bottom - rect.top) / containerElement.offsetHeight) : 1;
}
function getContainerRectangles() {
return {
rect: values.rect,
visibleRect: values.visibleRect
};
}
function getBeginEndOfDOMRect(rect) {
return {
begin: propMapper.get(rect, 'begin'),
end: propMapper.get(rect, 'end')
};
}
function getBeginEndOfContainer() {
const begin = propMapper.get(values.rect, 'begin') + values.translation;
const end = propMapper.get(values.rect, 'end') + values.translation;
return { begin, end };
}
function getBeginEndOfContainerVisibleRect() {
const begin = propMapper.get(values.visibleRect, 'begin') + values.translation;
const end = propMapper.get(values.visibleRect, 'end') + values.translation;
return { begin, end };
}
function getContainerScale() {
return { scaleX: values.scaleX, scaleY: values.scaleY };
}
function getSize(element) {
return propMapper.get(element, 'size') * propMapper.get(values, 'scale');
}
function getDistanceToOffsetParent(element) {
const distance = propMapper.get(element, 'distanceToParent') + (element[translationValue] || 0);
return distance * propMapper.get(values, 'scale');
}
function getBeginEnd(element) {
const begin = getDistanceToOffsetParent(element) + (propMapper.get(values.rect, 'begin') + values.translation) - propMapper.get(containerElement, 'scrollValue');
return {
begin,
end: begin + getSize(element) * propMapper.get(values, 'scale')
};
}
function setSize(element, size) {
propMapper.set(element, 'setSize', size);
}
function getAxisValue(position) {
return propMapper.get(position, 'dragPosition');
}
function updateDescendantContainerRects(container) {
container.layout.invalidateRects();
container.onTranslated();
if (container.getChildContainers()) {
container.getChildContainers().forEach(p => updateDescendantContainerRects(p));
}
}
function setTranslation(element, translation) {
if (!translation) {
element.style.removeProperty('transform');
} else {
propMapper.set(element.style, 'translate', translation);
}
element[translationValue] = translation;
if (element[containersInDraggable]) {
setTimeout(() => {
element[containersInDraggable].forEach(p => {
updateDescendantContainerRects(p);
});
}, animationDuration + 20);
}
}
function getTranslation(element) {
return element[translationValue];
}
function setVisibility(element, isVisible) {
if (element[visibilityValue] === undefined || element[visibilityValue] !== isVisible) {
if (isVisible) {
element.style.removeProperty('visibility');
} else {
element.style.visibility = 'hidden';
}
element[visibilityValue] = isVisible;
}
}
function isVisible(element) {
return element[visibilityValue] === undefined || element[visibilityValue];
}
function isInVisibleRect(x, y) {
let { left, top, right, bottom } = values.visibleRect;
// if there is no wrapper in rect size will be 0 and wont accept any drop
// so make sure at least there is 30px difference
if (bottom - top < 2) {
bottom = top + 30;
}
const containerRect = values.rect;
if (orientation === 'vertical') {
return x > containerRect.left && x < containerRect.right && y > top && y < bottom;
} else {
return x > left && x < right && y > containerRect.top && y < containerRect.bottom;
}
}
function setScrollListener(callback) {
registeredScrollListener = callback;
}
function getTopLeftOfElementBegin(begin) {
let top = 0;
let left = 0;
if (orientation === 'horizontal') {
left = begin;
top = values.rect.top;
} else {
left = values.rect.left;
top = begin;
}
return {
top, left
};
}
function getScrollSize(element) {
return propMapper.get(element, 'scrollSize');
}
function getScrollValue(element) {
return propMapper.get(element, 'scrollValue');
}
function setScrollValue(element, val) {
return propMapper.set(element, 'scrollValue', val);
}
function dispose() {
if (scrollListener) {
scrollListener.dispose();
}
if (visibleRect) {
visibleRect.parentNode.removeChild(visibleRect);
visibleRect = null;
}
}
function getPosition(position) {
return isInVisibleRect(position.x, position.y) ? getAxisValue(position) : null;
}
function invalidateRects() {
invalidateContainerRectangles(containerElement);
}
return {
getSize,
//getDistanceToContainerBegining,
getContainerRectangles,
getBeginEndOfDOMRect,
getBeginEndOfContainer,
getBeginEndOfContainerVisibleRect,
getBeginEnd,
getAxisValue,
setTranslation,
getTranslation,
setVisibility,
isVisible,
isInVisibleRect,
dispose,
getContainerScale,
setScrollListener,
setSize,
getTopLeftOfElementBegin,
getScrollSize,
getScrollValue,
setScrollValue,
invalidate,
invalidateRects,
getPosition,
};
}

View File

@@ -0,0 +1,479 @@
import './polyfills';
import * as Utils from './utils';
import * as constants from './constants';
import { addStyleToHead, addCursorStyleToBody, removeStyle } from './styles';
import dragScroller from './dragscroller';
const grabEvents = ['mousedown', 'touchstart'];
const moveEvents = ['mousemove', 'touchmove'];
const releaseEvents = ['mouseup', 'touchend'];
let dragListeningContainers = null;
let grabbedElement = null;
let ghostInfo = null;
let draggableInfo = null;
let containers = [];
let isDragging = false;
let removedElement = null;
let handleDrag = null;
let handleScroll = null;
let sourceContainer = null;
let sourceContainerLockAxis = null;
let cursorStyleElement = null;
// Utils.addClass(document.body, 'clearfix');
const isMobile = Utils.isMobile();
function listenEvents() {
if (typeof window !== 'undefined') {
addGrabListeners();
}
}
function addGrabListeners() {
grabEvents.forEach(e => {
global.document.addEventListener(e, onMouseDown, { passive: false });
});
}
function addMoveListeners() {
moveEvents.forEach(e => {
global.document.addEventListener(e, onMouseMove, { passive: false });
});
}
function removeMoveListeners() {
moveEvents.forEach(e => {
global.document.removeEventListener(e, onMouseMove, { passive: false });
});
}
function addReleaseListeners() {
releaseEvents.forEach(e => {
global.document.addEventListener(e, onMouseUp, { passive: false });
});
}
function removeReleaseListeners() {
releaseEvents.forEach(e => {
global.document.removeEventListener(e, onMouseUp, { passive: false });
});
}
function getGhostParent() {
if (draggableInfo.ghostParent) {
return draggableInfo.ghostParent;
}
if (grabbedElement) {
return grabbedElement.parentElement || global.document.body;
} else {
return global.document.body;
}
}
function getGhostElement(wrapperElement, { x, y }, container, cursor) {
const { scaleX = 1, scaleY = 1 } = container.getScale();
const { left, top, right, bottom } = wrapperElement.getBoundingClientRect();
const midX = left + (right - left) / 2;
const midY = top + (bottom - top) / 2;
const ghost = wrapperElement.cloneNode(true);
ghost.style.zIndex = 1000;
ghost.style.boxSizing = 'border-box';
ghost.style.position = 'fixed';
ghost.style.left = left + 'px';
ghost.style.top = top + 'px';
ghost.style.width = right - left + 'px';
ghost.style.height = bottom - top + 'px';
ghost.style.overflow = 'visible';
ghost.style.transition = null;
ghost.style.removeProperty('transition');
ghost.style.pointerEvents = 'none';
if (container.getOptions().dragClass) {
setTimeout(() => {
Utils.addClass(ghost.firstElementChild, container.getOptions().dragClass);
const dragCursor = global.getComputedStyle(ghost.firstElementChild).cursor;
cursorStyleElement = addCursorStyleToBody(dragCursor);
});
} else {
cursorStyleElement = addCursorStyleToBody(cursor);
}
Utils.addClass(ghost, container.getOptions().orientation);
Utils.addClass(ghost, constants.ghostClass);
return {
ghost: ghost,
centerDelta: { x: midX - x, y: midY - y },
positionDelta: { left: left - x, top: top - y }
};
}
function getDraggableInfo(draggableElement) {
const container = containers.filter(p => draggableElement.parentElement === p.element)[0];
const draggableIndex = container.draggables.indexOf(draggableElement);
const getGhostParent = container.getOptions().getGhostParent;
return {
container,
element: draggableElement,
elementIndex: draggableIndex,
payload: container.getOptions().getChildPayload
? container.getOptions().getChildPayload(draggableIndex)
: undefined,
targetElement: null,
position: { x: 0, y: 0 },
groupName: container.getOptions().groupName,
ghostParent: getGhostParent ? getGhostParent() : null,
};
}
function handleDropAnimation(callback) {
function endDrop() {
Utils.removeClass(ghostInfo.ghost, 'animated');
ghostInfo.ghost.style.transitionDuration = null;
getGhostParent().removeChild(ghostInfo.ghost);
callback();
}
function animateGhostToPosition({ top, left }, duration, dropClass) {
Utils.addClass(ghostInfo.ghost, 'animated');
if (dropClass) {
Utils.addClass(ghostInfo.ghost.firstElementChild, dropClass);
}
ghostInfo.ghost.style.transitionDuration = duration + 'ms';
ghostInfo.ghost.style.left = left + 'px';
ghostInfo.ghost.style.top = top + 'px';
setTimeout(function() {
endDrop();
}, duration + 20);
}
function shouldAnimateDrop(options) {
return options.shouldAnimateDrop
? options.shouldAnimateDrop(draggableInfo.container.getOptions(), draggableInfo.payload)
: true;
}
if (draggableInfo.targetElement) {
const container = containers.filter(p => p.element === draggableInfo.targetElement)[0];
if (shouldAnimateDrop(container.getOptions())) {
const dragResult = container.getDragResult();
animateGhostToPosition(
dragResult.shadowBeginEnd.rect,
Math.max(150, container.getOptions().animationDuration / 2),
container.getOptions().dropClass
);
} else {
endDrop();
}
} else {
const container = containers.filter(p => p === draggableInfo.container)[0];
const { behaviour, removeOnDropOut } = container.getOptions();
if (behaviour === 'move' && !removeOnDropOut && container.getDragResult()) {
const { removedIndex, elementSize } = container.getDragResult();
const layout = container.layout;
// drag ghost to back
container.getTranslateCalculator({
dragResult: {
removedIndex,
addedIndex: removedIndex,
elementSize
}
});
const prevDraggableEnd =
removedIndex > 0
? layout.getBeginEnd(container.draggables[removedIndex - 1]).end
: layout.getBeginEndOfContainer().begin;
animateGhostToPosition(
layout.getTopLeftOfElementBegin(prevDraggableEnd),
container.getOptions().animationDuration,
container.getOptions().dropClass
);
} else {
Utils.addClass(ghostInfo.ghost, 'animated');
ghostInfo.ghost.style.transitionDuration = container.getOptions().animationDuration + 'ms';
ghostInfo.ghost.style.opacity = '0';
ghostInfo.ghost.style.transform = 'scale(0.90)';
setTimeout(function() {
endDrop();
}, container.getOptions().animationDuration);
}
}
}
const handleDragStartConditions = (function handleDragStartConditions() {
let startEvent;
let delay;
let clb;
let timer = null;
const moveThreshold = 1;
const maxMoveInDelay = 5;
function onMove(event) {
const { clientX: currentX, clientY: currentY } = getPointerEvent(event);
if (!delay) {
if (
Math.abs(startEvent.clientX - currentX) > moveThreshold ||
Math.abs(startEvent.clientY - currentY) > moveThreshold
) {
return callCallback();
}
} else {
if (
Math.abs(startEvent.clientX - currentX) > maxMoveInDelay ||
Math.abs(startEvent.clientY - currentY) > maxMoveInDelay
) {
deregisterEvent();
}
}
}
function onUp() {
deregisterEvent();
}
function onHTMLDrag() {
deregisterEvent();
}
function registerEvents() {
if (delay) {
timer = setTimeout(callCallback, delay);
}
moveEvents.forEach(e => global.document.addEventListener(e, onMove), {
passive: false
});
releaseEvents.forEach(e => global.document.addEventListener(e, onUp), {
passive: false
});
global.document.addEventListener('drag', onHTMLDrag, {
passive: false
});
}
function deregisterEvent() {
clearTimeout(timer);
moveEvents.forEach(e => global.document.removeEventListener(e, onMove), {
passive: false
});
releaseEvents.forEach(e => global.document.removeEventListener(e, onUp), {
passive: false
});
global.document.removeEventListener('drag', onHTMLDrag, {
passive: false
});
}
function callCallback() {
clearTimeout(timer);
deregisterEvent();
clb();
}
return function(_startEvent, _delay, _clb) {
startEvent = getPointerEvent(_startEvent);
delay = (typeof _delay === 'number') ? _delay : (isMobile ? 200 : 0);
clb = _clb;
registerEvents();
};
})();
function onMouseDown(event) {
const e = getPointerEvent(event);
if (!isDragging && (e.button === undefined || e.button === 0)) {
grabbedElement = Utils.getParent(e.target, '.' + constants.wrapperClass);
if (grabbedElement) {
const containerElement = Utils.getParent(grabbedElement, '.' + constants.containerClass);
const container = containers.filter(p => p.element === containerElement)[0];
const dragHandleSelector = container.getOptions().dragHandleSelector;
const nonDragAreaSelector = container.getOptions().nonDragAreaSelector;
let startDrag = true;
if (dragHandleSelector && !Utils.getParent(e.target, dragHandleSelector)) {
startDrag = false;
}
if (nonDragAreaSelector && Utils.getParent(e.target, nonDragAreaSelector)) {
startDrag = false;
}
if (startDrag) {
handleDragStartConditions(e, container.getOptions().dragBeginDelay, () => {
Utils.clearSelection();
initiateDrag(e, Utils.getElementCursor(event.target));
addMoveListeners();
addReleaseListeners();
});
}
}
}
}
function onMouseUp() {
removeMoveListeners();
removeReleaseListeners();
handleScroll({ reset: true });
if (cursorStyleElement) {
removeStyle(cursorStyleElement);
cursorStyleElement = null;
}
if (draggableInfo) {
handleDropAnimation(() => {
Utils.removeClass(global.document.body, constants.disbaleTouchActions);
Utils.removeClass(global.document.body, constants.noUserSelectClass);
fireOnDragStartEnd(false);
(dragListeningContainers || []).forEach(p => {
p.handleDrop(draggableInfo);
});
dragListeningContainers = null;
grabbedElement = null;
ghostInfo = null;
draggableInfo = null;
isDragging = false;
sourceContainer = null;
sourceContainerLockAxis = null;
handleDrag = null;
});
}
}
function getPointerEvent(e) {
return e.touches ? e.touches[0] : e;
}
function dragHandler(dragListeningContainers) {
let targetContainers = dragListeningContainers;
return function(draggableInfo) {
let containerBoxChanged = false;
targetContainers.forEach(p => {
const dragResult = p.handleDrag(draggableInfo);
containerBoxChanged |= dragResult.containerBoxChanged || false;
dragResult.containerBoxChanged = false;
});
handleScroll({ draggableInfo });
if (containerBoxChanged) {
containerBoxChanged = false;
setTimeout(() => {
containers.forEach(p => {
p.layout.invalidateRects();
p.onTranslated();
});
}, 10);
}
};
}
function getScrollHandler(container, dragListeningContainers) {
if (container.getOptions().autoScrollEnabled) {
return dragScroller(dragListeningContainers);
} else {
return () => null;
}
}
function fireOnDragStartEnd(isStart) {
containers.forEach(p => {
const fn = isStart ? p.getOptions().onDragStart : p.getOptions().onDragEnd;
if (fn) {
const options = {
isSource: p === draggableInfo.container,
payload: draggableInfo.payload
};
if (p.isDragRelevant(draggableInfo.container, draggableInfo.payload)) {
options.willAcceptDrop = true;
} else {
options.willAcceptDrop = false;
}
fn(options);
}
});
}
function initiateDrag(position, cursor) {
isDragging = true;
const container = containers.filter(p => grabbedElement.parentElement === p.element)[0];
container.setDraggables();
sourceContainer = container;
sourceContainerLockAxis = container.getOptions().lockAxis ? container.getOptions().lockAxis.toLowerCase() : null;
draggableInfo = getDraggableInfo(grabbedElement);
ghostInfo = getGhostElement(
grabbedElement,
{ x: position.clientX, y: position.clientY },
draggableInfo.container,
cursor
);
draggableInfo.position = {
x: position.clientX + ghostInfo.centerDelta.x,
y: position.clientY + ghostInfo.centerDelta.y
};
draggableInfo.mousePosition = {
x: position.clientX,
y: position.clientY
};
Utils.addClass(global.document.body, constants.disbaleTouchActions);
Utils.addClass(global.document.body, constants.noUserSelectClass);
dragListeningContainers = containers.filter(p => p.isDragRelevant(container, draggableInfo.payload));
handleDrag = dragHandler(dragListeningContainers);
if (handleScroll) {
handleScroll({ reset: true });
}
handleScroll = getScrollHandler(container, dragListeningContainers);
dragListeningContainers.forEach(p => p.prepareDrag(p, dragListeningContainers));
fireOnDragStartEnd(true);
handleDrag(draggableInfo);
getGhostParent().appendChild(ghostInfo.ghost);
}
function onMouseMove(event) {
event.preventDefault();
const e = getPointerEvent(event);
if (!draggableInfo) {
initiateDrag(e, Utils.getElementCursor(event.target));
} else {
// just update ghost position && draggableInfo position
if (sourceContainerLockAxis) {
if (sourceContainerLockAxis === 'y') {
ghostInfo.ghost.style.top = `${e.clientY + ghostInfo.positionDelta.top}px`;
draggableInfo.position.y = e.clientY + ghostInfo.centerDelta.y;
draggableInfo.mousePosition.y = e.clientY;
} else if (sourceContainerLockAxis === 'x') {
ghostInfo.ghost.style.left = `${e.clientX + ghostInfo.positionDelta.left}px`;
draggableInfo.position.x = e.clientX + ghostInfo.centerDelta.x;
draggableInfo.mousePosition.x = e.clientX;
}
} else {
ghostInfo.ghost.style.left = `${e.clientX + ghostInfo.positionDelta.left}px`;
ghostInfo.ghost.style.top = `${e.clientY + ghostInfo.positionDelta.top}px`;
draggableInfo.position.x = e.clientX + ghostInfo.centerDelta.x;
draggableInfo.position.y = e.clientY + ghostInfo.centerDelta.y;
draggableInfo.mousePosition.x = e.clientX;
draggableInfo.mousePosition.y = e.clientY;
}
handleDrag(draggableInfo);
}
}
function Mediator() {
listenEvents();
return {
register: function(container) {
containers.push(container);
},
unregister: function(container) {
containers.splice(containers.indexOf(container), 1);
}
};
}
addStyleToHead();
export default Mediator();

View File

@@ -0,0 +1,17 @@
(function(constructor) {
if (constructor && constructor.prototype && !constructor.prototype.matches) {
constructor.prototype.matches =
constructor.prototype.matchesSelector ||
constructor.prototype.mozMatchesSelector ||
constructor.prototype.msMatchesSelector ||
constructor.prototype.oMatchesSelector ||
constructor.prototype.webkitMatchesSelector ||
function(s) {
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
i = matches.length;
while (--i >= 0 && matches.item(i) !== this) {}
return i > -1;
};
}
})(global.Node || global.Element);

View File

@@ -0,0 +1,118 @@
import * as constants from "./constants";
const verticalWrapperClass = {
overflow: "hidden",
display: "block"
};
const horizontalWrapperClass = {
height: "100%",
display: "inline-block",
"vertical-align": "top",
"white-space": "normal"
};
const stretcherElementHorizontalClass = {
display: "inline-block"
};
const css = {
[`.${constants.containerClass}`]: {
position: "relative"
},
[`.${constants.containerClass} *`]: {
"box-sizing": "border-box"
},
[`.${constants.containerClass}.horizontal`]: {
"white-space": "nowrap"
},
[`.${constants.containerClass}.horizontal > .${constants.stretcherElementClass}`]: stretcherElementHorizontalClass,
[`.${constants.containerClass}.horizontal > .${constants.wrapperClass}`]: horizontalWrapperClass,
[`.${constants.containerClass}.vertical > .${constants.wrapperClass}`]: verticalWrapperClass,
[`.${constants.wrapperClass}`]: {
// 'overflow': 'hidden'
},
[`.${constants.wrapperClass}.horizontal`]: horizontalWrapperClass,
[`.${constants.wrapperClass}.vertical`]: verticalWrapperClass,
[`.${constants.wrapperClass}.animated`]: {
transition: "transform ease"
},
[`.${constants.ghostClass} *`]: {
//'perspective': '800px',
"box-sizing": "border-box"
},
[`.${constants.ghostClass}.animated`]: {
transition: "all ease-in-out"
},
[`.${constants.disbaleTouchActions} *`]: {
"touch-actions": "none",
"-ms-touch-actions": "none"
},
[`.${constants.noUserSelectClass} *`]: {
"-webkit-touch-callout": "none",
"-webkit-user-select": "none",
"-khtml-user-select": "none",
"-moz-user-select": "none",
"-ms-user-select": "none",
"user-select": "none"
}
};
function convertToCssString(css) {
return Object.keys(css).reduce((styleString, propName) => {
const propValue = css[propName];
if (typeof propValue === "object") {
return `${styleString}${propName}{${convertToCssString(propValue)}}`;
}
return `${styleString}${propName}:${propValue};`;
}, "");
}
function addStyleToHead() {
if (typeof window !== "undefined") {
const head = global.document.head || global.document.getElementsByTagName("head")[0];
const style = global.document.createElement("style");
const cssString = convertToCssString(css);
style.type = "text/css";
if (style.styleSheet) {
style.styleSheet.cssText = cssString;
} else {
style.appendChild(global.document.createTextNode(cssString));
}
head.appendChild(style);
}
}
function addCursorStyleToBody(cursor) {
if (cursor && typeof window !== "undefined") {
const head = global.document.head || global.document.getElementsByTagName("head")[0];
const style = global.document.createElement("style");
const cssString = convertToCssString({
"body *": {
cursor: `${cursor} !important`
}
});
style.type = "text/css";
if (style.styleSheet) {
style.styleSheet.cssText = cssString;
} else {
style.appendChild(global.document.createTextNode(cssString));
}
head.appendChild(style);
return style;
}
return null;
}
function removeStyle(styleElement) {
if (styleElement && typeof window !== "undefined") {
const head = global.document.head || global.document.getElementsByTagName("head")[0];
head.removeChild(styleElement);
}
}
export { addStyleToHead, addCursorStyleToBody, removeStyle };

View File

@@ -0,0 +1,282 @@
export const getIntersection = (rect1, rect2) => {
return {
left: Math.max(rect1.left, rect2.left),
top: Math.max(rect1.top, rect2.top),
right: Math.min(rect1.right, rect2.right),
bottom: Math.min(rect1.bottom, rect2.bottom)
};
};
export const getIntersectionOnAxis = (rect1, rect2, axis) => {
if (axis === "x") {
return {
left: Math.max(rect1.left, rect2.left),
top: rect1.top,
right: Math.min(rect1.right, rect2.right),
bottom: rect1.bottom
};
} else {
return {
left: rect1.left,
top: Math.max(rect1.top, rect2.top),
right: rect1.right,
bottom: Math.min(rect1.bottom, rect2.bottom)
};
}
};
export const getContainerRect = element => {
const _rect = element.getBoundingClientRect();
const rect = {
left: _rect.left,
right: _rect.right + 10,
top: _rect.top,
bottom: _rect.bottom
};
if (hasBiggerChild(element, "x") && !isScrollingOrHidden(element, "x")) {
const width = rect.right - rect.left;
rect.right = rect.right + element.scrollWidth - width;
}
if (hasBiggerChild(element, "y") && !isScrollingOrHidden(element, "y")) {
const height = rect.bottom - rect.top;
rect.bottom = rect.bottom + element.scrollHeight - height;
}
return rect;
};
export const getScrollingAxis = element => {
const style = global.getComputedStyle(element);
const overflow = style["overflow"];
const general = overflow === "auto" || overflow === "scroll";
if (general) return "xy";
const overFlowX = style[`overflow-x`];
const xScroll = overFlowX === "auto" || overFlowX === "scroll";
const overFlowY = style[`overflow-y`];
const yScroll = overFlowY === "auto" || overFlowY === "scroll";
return `${xScroll ? "x" : ""}${yScroll ? "y" : ""}` || null;
};
export const isScrolling = (element, axis) => {
const style = global.getComputedStyle(element);
const overflow = style["overflow"];
const overFlowAxis = style[`overflow-${axis}`];
const general = overflow === "auto" || overflow === "scroll";
const dimensionScroll = overFlowAxis === "auto" || overFlowAxis === "scroll";
return general || dimensionScroll;
};
export const isScrollingOrHidden = (element, axis) => {
const style = global.getComputedStyle(element);
const overflow = style["overflow"];
const overFlowAxis = style[`overflow-${axis}`];
const general =
overflow === "auto" || overflow === "scroll" || overflow === "hidden";
const dimensionScroll =
overFlowAxis === "auto" ||
overFlowAxis === "scroll" ||
overFlowAxis === "hidden";
return general || dimensionScroll;
};
export const hasBiggerChild = (element, axis) => {
if (axis === "x") {
return element.scrollWidth > element.clientWidth;
} else {
return element.scrollHeight > element.clientHeight;
}
};
export const hasScrollBar = (element, axis) => {
return hasBiggerChild(element, axis) && isScrolling(element, axis);
};
export const getVisibleRect = (element, elementRect) => {
let currentElement = element;
let rect = elementRect || getContainerRect(element);
currentElement = element.parentElement;
while (currentElement) {
if (
hasBiggerChild(currentElement, "x") &&
isScrollingOrHidden(currentElement, "x")
) {
rect = getIntersectionOnAxis(
rect,
currentElement.getBoundingClientRect(),
"x"
);
}
if (
hasBiggerChild(currentElement, "y") &&
isScrollingOrHidden(currentElement, "y")
) {
rect = getIntersectionOnAxis(
rect,
currentElement.getBoundingClientRect(),
"y"
);
}
currentElement = currentElement.parentElement;
}
return rect;
};
export const listenScrollParent = (element, clb) => {
let scrollers = [];
const dispose = () => {
scrollers.forEach(p => {
p.removeEventListener("scroll", clb);
});
global.removeEventListener("scroll", clb);
};
setTimeout(function() {
let currentElement = element;
while (currentElement) {
if (
isScrolling(currentElement, "x") ||
isScrolling(currentElement, "y")
) {
currentElement.addEventListener("scroll", clb);
scrollers.push(currentElement);
}
currentElement = currentElement.parentElement;
}
global.addEventListener("scroll", clb);
}, 10);
return {
dispose
};
};
export const hasParent = (element, parent) => {
let current = element;
while (current) {
if (current === parent) {
return true;
}
current = current.parentElement;
}
return false;
};
export const getParent = (element, selector) => {
let current = element;
while (current) {
if (current.matches(selector)) {
return current;
}
current = current.parentElement;
}
return null;
};
export const hasClass = (element, cls) => {
return (
element.className
.split(" ")
.map(p => p)
.indexOf(cls) > -1
);
};
export const addClass = (element, cls) => {
if (element) {
element.className = element.className || ''
const classes = element.className.split(" ").filter(p => p);
if (classes.indexOf(cls) === -1) {
classes.unshift(cls);
element.className = classes.join(" ");
}
}
};
export const removeClass = (element, cls) => {
if (element) {
const classes = element.className.split(" ").filter(p => p && p !== cls);
element.className = classes.join(" ");
}
};
export const debounce = (fn, delay, immediate) => {
let timer = null;
return (...params) => {
if (timer) {
clearTimeout(timer);
}
if (immediate && !timer) {
fn.call(this, ...params);
} else {
timer = setTimeout(() => {
timer = null;
fn.call(this, ...params);
}, delay);
}
};
};
export const removeChildAt = (parent, index) => {
return parent.removeChild(parent.children[index]);
};
export const addChildAt = (parent, child, index) => {
if (index >= parent.children.lenght) {
parent.appendChild(child);
} else {
parent.insertBefore(child, parent.children[index]);
}
};
export const isMobile = () => {
if (typeof window !== 'undefined') {
if (
global.navigator.userAgent.match(/Android/i) ||
global.navigator.userAgent.match(/webOS/i) ||
global.navigator.userAgent.match(/iPhone/i) ||
global.navigator.userAgent.match(/iPad/i) ||
global.navigator.userAgent.match(/iPod/i) ||
global.navigator.userAgent.match(/BlackBerry/i) ||
global.navigator.userAgent.match(/Windows Phone/i)
) {
return true;
} else {
return false;
}
}
return false;
};
export const clearSelection = () => {
if (global.getSelection) {
if (global.getSelection().empty) {
// Chrome
global.getSelection().empty();
} else if (global.getSelection().removeAllRanges) {
// Firefox
global.getSelection().removeAllRanges();
}
} else if (global.document.selection) {
// IE?
global.document.selection.empty();
}
};
export const getElementCursor = (element) => {
if (element) {
const style = global.getComputedStyle(element);
if (style) {
return style.cursor;
}
}
return null;
}

View File

@@ -0,0 +1,361 @@
import { PopoverContainer, PopoverContent } from "react-popopo";
import styled, { createGlobalStyle, css } from "styled-components";
const getBoardWrapperStyles = (props) => {
if (props.orientation === "vertical") {
return ` `;
}
if (props.orientation === "horizontal") {
return `
display: flex;
flex-direction: row;
align-items: flex-start;
`;
}
return "";
};
const getSectionStyles = (props) => {
if (props.orientation === "horizontal") {
return `
display: inline-flex;
`;
}
return `
margin-bottom: 10px;
`;
};
export const GlobalStyle = createGlobalStyle`
.comPlainTextContentEditable {
-webkit-user-modify: read-write-plaintext-only;
cursor: text;
}
.smooth-dnd-container.horizontal {
}
.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;
}
.react-trello-column-header {
border-radius: 5px;
}
`;
export const StyleHorizontal = styled.div``;
export const StyleVertical = styled.div`
.react-trello-column-header {
text-align: left;
}
.smooth-dnd-container {
// TODO ? This is the question. We need the same drag-zone we get in horizontal mode
min-height: 50px; // Not needed, just for extra landing space
}
.smooth-dnd-container.horizontal {
// TODO: This is what is currently providing us multi row cols, and may need to be adjusted with new DND Library
display: flex; /* Allows wrapping */
flex-wrap: wrap; /* Allows wrapping */
//background-color: yellow !important;
}
.smooth-dnd-ghost {
//background-color: red !important;
}
.react-trello-card {
//background-color: orange !important;
margin: 5px;
// TODO: This is what is currently providing us multi row cols, and may need to be adjusted with new DND Library
flex: 0 1 auto;
}
.smooth-dnd-stretcher-element {
//background-color: purple !important;
}
.smooth-dnd-draggable-wrapper {
//background-color: blue !important;
flex: 0 1 auto; /* Allows items to grow and shrink */
}
.react-trello-board {
overflow-y: hidden !important;
}
`;
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: #ffffff;
overflow-y: scroll;
padding: 5px;
color: #393939;
${getBoardWrapperStyles};
`;
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: 2px 2px;
position: relative;
padding: 5px;
flex-direction: column;
${getSectionStyles};
`;
export const LaneHeader = styled(Header)`
margin-bottom: 0;
${(props) =>
props.editLaneTitle &&
css`
padding: 0;
line-height: 30px;
`} ${(props) =>
!props.editLaneTitle &&
css`
padding: 0 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;
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,15 @@
import React from "react";
import { DeleteWrapper } from "../styles/Elements";
import { Button } from "antd";
const DeleteButton = (props) => {
return (
<DeleteWrapper {...props}>
<Button type="primary" danger>
Delete
</Button>
</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,11 @@
import DeleteButton from "./DeleteButton";
import EditableLabel from "./EditableLabel";
import InlineInput from "./InlineInput";
const exports = {
DeleteButton,
EditableLabel,
InlineInput
};
export default exports;

View File

@@ -45,7 +45,7 @@ if (import.meta.env.PROD) {
maskAllText: false,
blockAllMedia: true
}),
new Sentry.BrowserTracing({})
new Sentry.browserTracingIntegration()
],
tracePropagationTargets: [
"api.imex.online",

View File

@@ -13,7 +13,10 @@ import FormsFieldChanged from "../../components/form-fields-changed-alert/form-f
export default function JobsCreateComponent({ form }) {
const [pageIndex, setPageIndex] = useState(0);
const [errorMessage, setErrorMessage] = useState(null);
// const [errorMessage, setErrorMessage] = useState(null);
const [errorMessage] = useState(null);
const [state] = useContext(JobCreateContext);
const { t } = useTranslation();

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

@@ -29,7 +29,9 @@ export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false
serializableCheck: false,
// TODO: (Note) This is a production board change
immutableCheck: false
}).concat(middlewares),
// middleware: middlewares,
devTools: import.meta.env.DEV,

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

2595
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,46 +19,43 @@
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.525.0",
"@aws-sdk/client-ses": "^3.525.0",
"@aws-sdk/credential-provider-node": "^3.525.0",
"@azure/storage-blob": "^12.17.0",
"@opensearch-project/opensearch": "^2.5.0",
"aws4": "^1.12.0",
"axios": "^1.6.5",
"@aws-sdk/client-secrets-manager": "^3.583.0",
"@aws-sdk/client-ses": "^3.583.0",
"@aws-sdk/credential-provider-node": "^3.583.0",
"@opensearch-project/opensearch": "^2.8.0",
"aws4": "^1.13.0",
"axios": "^1.7.2",
"better-queue": "^3.8.12",
"bluebird": "^3.7.2",
"body-parser": "^1.20.2",
"cloudinary": "^2.0.2",
"cloudinary": "^2.2.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cors": "2.8.5",
"csrf": "^3.1.0",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"firebase-admin": "^12.0.0",
"express": "^4.19.2",
"firebase-admin": "^12.1.1",
"graphql": "^16.8.1",
"graphql-request": "^6.1.0",
"graylog2": "^0.2.1",
"inline-css": "^4.0.2",
"intuit-oauth": "^4.0.0",
"json-2-csv": "^5.5.0",
"intuit-oauth": "^4.1.2",
"json-2-csv": "^5.5.1",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"multer": "^1.4.5-lts.1",
"node-mailjet": "^6.0.5",
"node-persist": "^4.0.1",
"node-quickbooks": "^2.0.44",
"nodemailer": "^6.9.11",
"phone": "^3.1.42",
"nodemailer": "^6.9.13",
"phone": "^3.1.44",
"recursive-diff": "^1.0.9",
"rimraf": "^5.0.5",
"soap": "^1.0.0",
"socket.io": "^4.7.4",
"rimraf": "^5.0.7",
"soap": "^1.0.3",
"socket.io": "^4.7.5",
"ssh2-sftp-client": "^10.0.3",
"stripe": "^14.19.0",
"twilio": "^4.23.0",
"uuid": "^9.0.1",
"xml2js": "^0.6.2",

View File

@@ -1,3 +1,8 @@
/**
* THIS FILE IS CURRENTLY DEPRECATED AND NOT IN USE
* If required, remember to re-install stripe 14.19.0
*/
const path = require("path");
require("dotenv").config({