Compare commits
27 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cf4a50a83 | ||
|
|
d846894d22 | ||
|
|
8f031a78c1 | ||
|
|
69b36a4c34 | ||
|
|
c7b8df5655 | ||
|
|
d85768b2ac | ||
|
|
a569c1f4f9 | ||
|
|
07a8e5b216 | ||
|
|
38bf58c613 | ||
|
|
ba90d72d55 | ||
|
|
9889bee924 | ||
|
|
a19e4e8f16 | ||
|
|
5121852fbc | ||
|
|
ec00697d31 | ||
|
|
c25714b68e | ||
|
|
dc0147c5f9 | ||
|
|
296afdbeee | ||
|
|
2f8f058c5c | ||
|
|
68784018e6 | ||
|
|
19dfec2a34 | ||
|
|
55d729339f | ||
|
|
c3108a17f4 | ||
|
|
d47ae64bd6 | ||
|
|
095e1e9789 | ||
|
|
a0b9f99dd3 | ||
|
|
a33a92207b | ||
|
|
f647e1ff11 |
@@ -2,7 +2,7 @@ NGROK TEsting:
|
||||
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
||||
|
||||
Finding deadfiles - run from client directory
|
||||
npx deadfile ./src/index.js --exclude build templates
|
||||
npx deadfile ./src/index.jsx --exclude build templates
|
||||
|
||||
#Crushing all hasura migrations by creating a new initialization from the server.
|
||||
hasura migrate create "Init" --from-server --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
||||
@@ -11,4 +11,4 @@ Production-ImEXOnline!@#'
|
||||
hasura migrate status --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
||||
|
||||
Generate the license file:
|
||||
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite
|
||||
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite
|
||||
|
||||
@@ -4,7 +4,7 @@ Clone Repository for:
|
||||
{
|
||||
"name": "node-webhook-scripts",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"main": "index.jsx",
|
||||
"dependencies": {
|
||||
"express": "^4.16.4"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ module.exports = {
|
||||
|
||||
{
|
||||
name: "Bitbucket Webhook",
|
||||
script: "./webhook/index.js",
|
||||
script: "./webhook/index.jsx",
|
||||
env: {
|
||||
NODE_ENV: "production"
|
||||
}
|
||||
|
||||
@@ -1,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"
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
// This example plugins/index.jsx can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// This example support/index.jsx is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
|
||||
22214
client/package-lock.json
generated
22214
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16422,7 +16422,7 @@ For when you don't want to write the same thing over and over to cache a method
|
||||
$ npm install --save-dev stubs
|
||||
```
|
||||
```js
|
||||
var mylib = require('./lib/index.js')
|
||||
var mylib = require('./lib/index.jsx')
|
||||
var stubs = require('stubs')
|
||||
|
||||
// make it a noop
|
||||
|
||||
@@ -16567,7 +16567,7 @@ even more slower.
|
||||
## Benchmarks
|
||||
|
||||
```bash
|
||||
$ node benchmarks/index.js
|
||||
$ node benchmarks/index.jsx
|
||||
Benchmarking: sign
|
||||
elliptic#sign x 262 ops/sec ±0.51% (177 runs sampled)
|
||||
eccjs#sign x 55.91 ops/sec ±0.90% (144 runs sampled)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ const CardColorLegend = ({ bodyshop }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Col>
|
||||
<Col style={{ marginLeft: "15px" }}>
|
||||
<Typography>{t("production.labels.legend")}</Typography>
|
||||
<List
|
||||
grid={{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
94881
client/src/components/production-board-kanban/testData/board1200.json
Normal file
94881
client/src/components/production-board-kanban/testData/board1200.json
Normal file
File diff suppressed because it is too large
Load Diff
15881
client/src/components/production-board-kanban/testData/board300.json
Normal file
15881
client/src/components/production-board-kanban/testData/board300.json
Normal file
File diff suppressed because it is too large
Load Diff
47481
client/src/components/production-board-kanban/testData/board600.json
Normal file
47481
client/src/components/production-board-kanban/testData/board600.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import { AddCardLink } from "../styles/Base";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const AddCardLinkComponent = ({ onClick, laneId }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <AddCardLink onClick={onClick}>{t("trello.labels.add_card")}</AddCardLink>;
|
||||
};
|
||||
|
||||
export default AddCardLinkComponent;
|
||||
112
client/src/components/trello-board/components/Card.jsx
Normal file
112
client/src/components/trello-board/components/Card.jsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import React, { useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { CardHeader, CardRightContent, CardTitle, Detail, Footer, MovableCardWrapper } from "../styles/Base";
|
||||
import InlineInput from "../widgets/InlineInput.jsx";
|
||||
import Tag from "./Card/Tag.jsx";
|
||||
import DeleteButton from "../widgets/DeleteButton.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Card = ({
|
||||
showDeleteButton = true,
|
||||
onDelete = () => {},
|
||||
onClick = () => {},
|
||||
style = {},
|
||||
tagStyle = {},
|
||||
className = "",
|
||||
id,
|
||||
title = "no title",
|
||||
label = "",
|
||||
description = "",
|
||||
tags = [],
|
||||
cardDraggable,
|
||||
editable,
|
||||
onChange
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(e) => {
|
||||
onDelete();
|
||||
e.stopPropagation();
|
||||
},
|
||||
[onDelete]
|
||||
);
|
||||
|
||||
const updateCard = (card) => {
|
||||
onChange({ ...card, id });
|
||||
};
|
||||
|
||||
return (
|
||||
<MovableCardWrapper data-id={id} onClick={onClick} style={style} className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle draggable={cardDraggable}>
|
||||
{editable ? (
|
||||
<InlineInput
|
||||
value={title}
|
||||
border
|
||||
placeholder={t("trello.labels.title")}
|
||||
resize="vertical"
|
||||
onSave={(value) => updateCard({ title: value })}
|
||||
/>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardRightContent>
|
||||
{editable ? (
|
||||
<InlineInput
|
||||
value={label}
|
||||
border
|
||||
placeholder={t("trello.labels.label")}
|
||||
resize="vertical"
|
||||
onSave={(value) => updateCard({ label: value })}
|
||||
/>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</CardRightContent>
|
||||
{showDeleteButton && <DeleteButton onClick={handleDelete} />}
|
||||
</CardHeader>
|
||||
<Detail>
|
||||
{editable ? (
|
||||
<InlineInput
|
||||
value={description}
|
||||
border
|
||||
placeholder={t("trello.labels.description")}
|
||||
resize="vertical"
|
||||
onSave={(value) => updateCard({ description: value })}
|
||||
/>
|
||||
) : (
|
||||
description
|
||||
)}
|
||||
</Detail>
|
||||
{tags && tags.length > 0 && (
|
||||
<Footer>
|
||||
{tags.map((tag) => (
|
||||
<Tag key={tag.title} {...tag} tagStyle={tagStyle} />
|
||||
))}
|
||||
</Footer>
|
||||
)}
|
||||
</MovableCardWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
showDeleteButton: PropTypes.bool,
|
||||
onDelete: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
tagStyle: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
tags: PropTypes.array,
|
||||
cardDraggable: PropTypes.bool,
|
||||
editable: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Card;
|
||||
21
client/src/components/trello-board/components/Card/Tag.jsx
Normal file
21
client/src/components/trello-board/components/Card/Tag.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { TagSpan } from "../../styles/Base";
|
||||
|
||||
const Tag = ({ title, color, bgcolor, tagStyle, ...otherProps }) => {
|
||||
const style = { color: color || "white", backgroundColor: bgcolor || "orange", ...tagStyle };
|
||||
return (
|
||||
<TagSpan style={style} {...otherProps}>
|
||||
{title}
|
||||
</TagSpan>
|
||||
);
|
||||
};
|
||||
|
||||
Tag.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
color: PropTypes.string,
|
||||
bgcolor: PropTypes.string,
|
||||
tagStyle: PropTypes.object
|
||||
};
|
||||
|
||||
export default Tag;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
import { LaneFooter } from "../../styles/Base";
|
||||
import { CollapseBtn, ExpandBtn } from "../../styles/Elements";
|
||||
|
||||
const LaneFooterComponent = ({ onClick, collapsed }) => (
|
||||
<LaneFooter onClick={onClick}>{collapsed ? <ExpandBtn /> : <CollapseBtn />}</LaneFooter>
|
||||
);
|
||||
|
||||
export default LaneFooterComponent;
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import InlineInput from "../../widgets/InlineInput.jsx";
|
||||
import { LaneHeader, RightContent, Title } from "../../styles/Base";
|
||||
import LaneMenu from "./LaneHeader/LaneMenu.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const LaneHeaderComponent = ({
|
||||
updateTitle,
|
||||
canAddLanes,
|
||||
onDelete,
|
||||
onDoubleClick,
|
||||
editLaneTitle,
|
||||
label,
|
||||
title,
|
||||
titleStyle,
|
||||
labelStyle,
|
||||
laneDraggable
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<LaneHeader onDoubleClick={onDoubleClick} editLaneTitle={editLaneTitle}>
|
||||
<Title draggable={laneDraggable} style={titleStyle}>
|
||||
{editLaneTitle ? (
|
||||
<InlineInput
|
||||
value={title}
|
||||
border
|
||||
placeholder={t("trello.labels.title")}
|
||||
resize="vertical"
|
||||
onSave={updateTitle}
|
||||
/>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</Title>
|
||||
{label && (
|
||||
<RightContent>
|
||||
<span style={labelStyle}>{label}</span>
|
||||
</RightContent>
|
||||
)}
|
||||
{canAddLanes && <LaneMenu onDelete={onDelete} />}
|
||||
</LaneHeader>
|
||||
);
|
||||
};
|
||||
|
||||
LaneHeaderComponent.propTypes = {
|
||||
updateTitle: PropTypes.func,
|
||||
editLaneTitle: PropTypes.bool,
|
||||
canAddLanes: PropTypes.bool,
|
||||
laneDraggable: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
onDelete: PropTypes.func,
|
||||
onDoubleClick: PropTypes.func
|
||||
};
|
||||
|
||||
LaneHeaderComponent.defaultProps = {
|
||||
updateTitle: () => {},
|
||||
editLaneTitle: false,
|
||||
canAddLanes: false
|
||||
};
|
||||
|
||||
export default LaneHeaderComponent;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
|
||||
import { Popover } from "react-popopo";
|
||||
|
||||
import { CustomPopoverContainer, CustomPopoverContent } from "../../../styles/Base";
|
||||
|
||||
import {
|
||||
DeleteWrapper,
|
||||
GenDelButton,
|
||||
LaneMenuContent,
|
||||
LaneMenuHeader,
|
||||
LaneMenuItem,
|
||||
LaneMenuTitle,
|
||||
MenuButton
|
||||
} from "../../../styles/Elements";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const LaneMenu = ({ onDelete }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Popover
|
||||
position="bottom"
|
||||
PopoverContainer={CustomPopoverContainer}
|
||||
PopoverContent={CustomPopoverContent}
|
||||
trigger={<MenuButton>⋮</MenuButton>}
|
||||
>
|
||||
<LaneMenuHeader>
|
||||
<LaneMenuTitle>{t("trello.labels.lane_actions")}</LaneMenuTitle>
|
||||
<DeleteWrapper>
|
||||
<GenDelButton>✖</GenDelButton>
|
||||
</DeleteWrapper>
|
||||
</LaneMenuHeader>
|
||||
<LaneMenuContent>
|
||||
<LaneMenuItem onClick={onDelete}>{t("trello.labels.delete_lane")}</LaneMenuItem>
|
||||
</LaneMenuContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default LaneMenu;
|
||||
13
client/src/components/trello-board/components/Loader.jsx
Normal file
13
client/src/components/trello-board/components/Loader.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import {LoaderDiv, LoadingBar} from '../styles/Loader'
|
||||
|
||||
const Loader = () => (
|
||||
<LoaderDiv>
|
||||
<LoadingBar />
|
||||
<LoadingBar />
|
||||
<LoadingBar />
|
||||
<LoadingBar />
|
||||
</LoaderDiv>
|
||||
)
|
||||
|
||||
export default Loader
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { CardForm, CardHeader, CardRightContent, CardTitle, CardWrapper, Detail } from "../styles/Base";
|
||||
import { AddButton, CancelButton } from "../styles/Elements";
|
||||
import EditableLabel from "../widgets/EditableLabel.jsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const NewCardForm = ({ onCancel, onAdd }) => {
|
||||
const [state, setState] = useState({});
|
||||
const { t } = useTranslation();
|
||||
|
||||
const updateField = (field, value) => {
|
||||
setState((prevState) => ({ ...prevState, [field]: value }));
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
onAdd(state);
|
||||
};
|
||||
|
||||
return (
|
||||
<CardForm>
|
||||
<CardWrapper>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<EditableLabel
|
||||
placeholder={t("trello.labels.title")}
|
||||
onChange={(val) => updateField("title", val)}
|
||||
autoFocus
|
||||
/>
|
||||
</CardTitle>
|
||||
<CardRightContent>
|
||||
<EditableLabel placeholder={t("trello.labels.label")} onChange={(val) => updateField("label", val)} />
|
||||
</CardRightContent>
|
||||
</CardHeader>
|
||||
<Detail>
|
||||
<EditableLabel
|
||||
placeholder={t("trello.labels.description")}
|
||||
onChange={(val) => updateField("description", val)}
|
||||
/>
|
||||
</Detail>
|
||||
</CardWrapper>
|
||||
<AddButton onClick={handleAdd}>{t("trello.labels.add_card")}</AddButton>
|
||||
<CancelButton onClick={onCancel}>{t("trello.labels.cancel")}</CancelButton>
|
||||
</CardForm>
|
||||
);
|
||||
};
|
||||
|
||||
NewCardForm.propTypes = {
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default NewCardForm;
|
||||
@@ -0,0 +1,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;
|
||||
@@ -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;
|
||||
28
client/src/components/trello-board/components/index.js
Normal file
28
client/src/components/trello-board/components/index.js
Normal 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;
|
||||
28
client/src/components/trello-board/controllers/Board.jsx
Normal file
28
client/src/components/trello-board/controllers/Board.jsx
Normal 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;
|
||||
@@ -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;
|
||||
408
client/src/components/trello-board/controllers/Lane.jsx
Normal file
408
client/src/components/trello-board/controllers/Lane.jsx
Normal 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);
|
||||
111
client/src/components/trello-board/dnd/Container.jsx
Normal file
111
client/src/components/trello-board/dnd/Container.jsx
Normal 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;
|
||||
34
client/src/components/trello-board/dnd/Draggable.jsx
Normal file
34
client/src/components/trello-board/dnd/Draggable.jsx
Normal 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;
|
||||
118
client/src/components/trello-board/helpers/LaneHelper.js
Normal file
118
client/src/components/trello-board/helpers/LaneHelper.js
Normal 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;
|
||||
35
client/src/components/trello-board/index.jsx
Normal file
35
client/src/components/trello-board/index.jsx
Normal 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;
|
||||
8
client/src/components/trello-board/smooth-dnd/index.js
Normal file
8
client/src/components/trello-board/smooth-dnd/index.js
Normal 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,
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
} */
|
||||
777
client/src/components/trello-board/smooth-dnd/src/container.js
Normal file
777
client/src/components/trello-board/smooth-dnd/src/container.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
479
client/src/components/trello-board/smooth-dnd/src/mediator.js
Normal file
479
client/src/components/trello-board/smooth-dnd/src/mediator.js
Normal 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();
|
||||
@@ -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);
|
||||
|
||||
118
client/src/components/trello-board/smooth-dnd/src/styles.js
Normal file
118
client/src/components/trello-board/smooth-dnd/src/styles.js
Normal 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 };
|
||||
282
client/src/components/trello-board/smooth-dnd/src/utils.js
Normal file
282
client/src/components/trello-board/smooth-dnd/src/utils.js
Normal 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;
|
||||
}
|
||||
361
client/src/components/trello-board/styles/Base.js
Normal file
361
client/src/components/trello-board/styles/Base.js
Normal 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;
|
||||
}
|
||||
`;
|
||||
251
client/src/components/trello-board/styles/Elements.js
Normal file
251
client/src/components/trello-board/styles/Elements.js
Normal file
@@ -0,0 +1,251 @@
|
||||
import styled from 'styled-components'
|
||||
import {CardWrapper, MovableCardWrapper} from './Base'
|
||||
|
||||
export const DeleteWrapper = styled.div`
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: 2px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
export const GenDelButton = styled.button`
|
||||
transition: all 0.5s ease;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
font-size: 15px;
|
||||
height: 15px;
|
||||
padding: 0;
|
||||
margin-top: 5px;
|
||||
text-align: center;
|
||||
width: 15px;
|
||||
background: inherit;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
export const DelButton = styled.button`
|
||||
transition: all 0.5s ease;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
font-size: 8px;
|
||||
height: 15px;
|
||||
line-height: 1px;
|
||||
margin: 0 0 8px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: 15px;
|
||||
background: inherit;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
|
||||
${MovableCardWrapper}:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
export const MenuButton = styled.button`
|
||||
transition: all 0.5s ease;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
height: 15px;
|
||||
line-height: 1px;
|
||||
margin: 0 0 8px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: 15px;
|
||||
background: inherit;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
export const LaneMenuHeader = styled.div`
|
||||
position: relative;
|
||||
margin-bottom: 4px;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
export const LaneMenuContent = styled.div`
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 0 12px 12px;
|
||||
`
|
||||
|
||||
export const LaneMenuItem = styled.div`
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
padding: 6px 12px;
|
||||
position: relative;
|
||||
margin: 0 -12px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #3179ba;
|
||||
color: #fff;
|
||||
}
|
||||
`
|
||||
|
||||
export const LaneMenuTitle = styled.span`
|
||||
box-sizing: border-box;
|
||||
color: #6b808c;
|
||||
display: block;
|
||||
line-height: 30px;
|
||||
border-bottom: 1px solid rgba(9, 45, 66, 0.13);
|
||||
margin: 0 6px;
|
||||
overflow: hidden;
|
||||
padding: 0 32px;
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
export const DeleteIcon = styled.span`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
border: 1px solid #83bd42;
|
||||
border-radius: 50%;
|
||||
padding: 4px;
|
||||
background-color: #83bd42;
|
||||
|
||||
${CardWrapper}:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover::before,
|
||||
&:hover::after {
|
||||
background: red;
|
||||
}
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
width: 60%;
|
||||
top: 45%;
|
||||
left: 20%;
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-o-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&:after {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
-moz-transform: rotate(-45deg);
|
||||
-o-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
`
|
||||
|
||||
export const ExpandCollapseBase = styled.span`
|
||||
width: 36px;
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
export const CollapseBtn = styled(ExpandCollapseBase)`
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-bottom: 7px solid #444;
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
border-bottom: 3px solid #e3e3e3;
|
||||
border-left: 3px solid transparent;
|
||||
border-right: 3px solid transparent;
|
||||
}
|
||||
`
|
||||
|
||||
export const ExpandBtn = styled(ExpandCollapseBase)`
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-top: 7px solid #444;
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 0px;
|
||||
border-top: 3px solid #e3e3e3;
|
||||
border-left: 3px solid transparent;
|
||||
border-right: 3px solid transparent;
|
||||
}
|
||||
`
|
||||
|
||||
export const AddButton = styled.button`
|
||||
background: #5aac44;
|
||||
color: #fff;
|
||||
transition: background 0.3s ease;
|
||||
min-height: 32px;
|
||||
padding: 4px 16px;
|
||||
vertical-align: top;
|
||||
margin-top: 0;
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0;
|
||||
`
|
||||
|
||||
export const CancelButton = styled.button`
|
||||
background: #999999;
|
||||
color: #fff;
|
||||
transition: background 0.3s ease;
|
||||
min-height: 32px;
|
||||
padding: 4px 16px;
|
||||
vertical-align: top;
|
||||
margin-top: 0;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0;
|
||||
`
|
||||
export const AddLaneLink = styled.button`
|
||||
background: #2b6aa3;
|
||||
border: none;
|
||||
color: #fff;
|
||||
transition: background 0.3s ease;
|
||||
min-height: 32px;
|
||||
padding: 4px 16px;
|
||||
vertical-align: top;
|
||||
margin-top: 0;
|
||||
margin-right: 0px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0;
|
||||
`
|
||||
43
client/src/components/trello-board/styles/Loader.js
Normal file
43
client/src/components/trello-board/styles/Loader.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import styled, {keyframes} from 'styled-components'
|
||||
|
||||
const keyframeAnimation = keyframes`
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
transform: scale(1, 2.2);
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
}
|
||||
`
|
||||
export const LoaderDiv = styled.div`
|
||||
text-align: center;
|
||||
margin: 15px 0;
|
||||
`
|
||||
|
||||
export const LoadingBar = styled.div`
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
animation: ${keyframeAnimation} 1s ease-in-out infinite;
|
||||
background-color: #777;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: 0.0001s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.09s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.18s;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
animation-delay: 0.27s;
|
||||
}
|
||||
`
|
||||
15
client/src/components/trello-board/widgets/DeleteButton.jsx
Normal file
15
client/src/components/trello-board/widgets/DeleteButton.jsx
Normal 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;
|
||||
87
client/src/components/trello-board/widgets/EditableLabel.jsx
Normal file
87
client/src/components/trello-board/widgets/EditableLabel.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class EditableLabel extends React.Component {
|
||||
constructor({value}) {
|
||||
super()
|
||||
this.state = {value: value}
|
||||
}
|
||||
|
||||
getText = el => {
|
||||
return el.innerText
|
||||
}
|
||||
|
||||
onTextChange = ev => {
|
||||
const value = this.getText(ev.target)
|
||||
this.setState({value: value})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.autoFocus) {
|
||||
this.refDiv.focus()
|
||||
}
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
this.props.onChange(this.state.value)
|
||||
}
|
||||
|
||||
onPaste = ev => {
|
||||
ev.preventDefault()
|
||||
const value = ev.clipboardData.getData('text')
|
||||
document.execCommand('insertText', false, value)
|
||||
}
|
||||
|
||||
getClassName = () => {
|
||||
const placeholder = this.state.value === '' ? 'comPlainTextContentEditable--has-placeholder' : ''
|
||||
return `comPlainTextContentEditable ${placeholder}`
|
||||
}
|
||||
|
||||
onKeyDown = e => {
|
||||
if (e.keyCode === 13) {
|
||||
this.props.onChange(this.state.value)
|
||||
this.refDiv.blur()
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
this.refDiv.value = this.props.value
|
||||
this.setState({value: this.props.value})
|
||||
// this.refDiv.blur()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const placeholder = this.props.value.length > 0 ? false : this.props.placeholder
|
||||
return (
|
||||
<div
|
||||
ref={ref => (this.refDiv = ref)}
|
||||
contentEditable="true"
|
||||
className={this.getClassName()}
|
||||
onPaste={this.onPaste}
|
||||
onBlur={this.onBlur}
|
||||
onInput={this.onTextChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
EditableLabel.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
autoFocus: PropTypes.bool,
|
||||
inline: PropTypes.bool,
|
||||
value: PropTypes.string
|
||||
}
|
||||
|
||||
EditableLabel.defaultProps = {
|
||||
onChange: () => {},
|
||||
placeholder: '',
|
||||
autoFocus: false,
|
||||
inline: false,
|
||||
value: ''
|
||||
}
|
||||
export default EditableLabel
|
||||
106
client/src/components/trello-board/widgets/InlineInput.jsx
Normal file
106
client/src/components/trello-board/widgets/InlineInput.jsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { InlineInput } from "../styles/Base";
|
||||
import autosize from "autosize";
|
||||
|
||||
const InlineInputController = ({ onSave, border, placeholder, value, autoFocus, resize, onCancel }) => {
|
||||
const inputRef = useRef(null);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
|
||||
// Effect for autosizing and initial autoFocus
|
||||
useEffect(() => {
|
||||
if (inputRef.current && resize !== "none") {
|
||||
autosize(inputRef.current);
|
||||
}
|
||||
if (inputRef.current && autoFocus) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [resize, autoFocus]);
|
||||
|
||||
// Effect to update value when props change
|
||||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleFocus = (e) => e.target.select();
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
if (document.activeElement !== e.target) {
|
||||
e.preventDefault();
|
||||
inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
updateValue();
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
// Enter
|
||||
inputRef.current.blur();
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode === 27) {
|
||||
// Escape
|
||||
setInputValue(value); // Reset to initial value
|
||||
inputRef.current.blur();
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode === 9) {
|
||||
// Tab
|
||||
if (inputValue.length === 0) {
|
||||
onCancel();
|
||||
}
|
||||
inputRef.current.blur();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const updateValue = () => {
|
||||
if (inputValue !== value) {
|
||||
onSave(inputValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineInput
|
||||
ref={inputRef}
|
||||
border={border}
|
||||
onMouseDown={handleMouseDown}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={inputValue.length === 0 ? undefined : placeholder}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
dataGramm="false"
|
||||
rows={1}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
InlineInputController.propTypes = {
|
||||
onSave: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
border: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
autoFocus: PropTypes.bool,
|
||||
resize: PropTypes.oneOf(["none", "vertical", "horizontal"])
|
||||
};
|
||||
|
||||
InlineInputController.defaultProps = {
|
||||
onSave: () => {},
|
||||
onCancel: () => {},
|
||||
placeholder: "",
|
||||
value: "",
|
||||
border: false,
|
||||
autoFocus: false,
|
||||
resize: "none"
|
||||
};
|
||||
|
||||
export default InlineInputController;
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { InlineInput } from "../styles/Base";
|
||||
import autosize from "autosize";
|
||||
|
||||
class NewLaneTitleEditor extends React.Component {
|
||||
onKeyDown = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
this.refInput.blur();
|
||||
this.props.onSave();
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
this.cancel();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (e.keyCode === 9) {
|
||||
if (this.getValue().length === 0) {
|
||||
this.cancel();
|
||||
} else {
|
||||
this.props.onSave();
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
this.setValue("");
|
||||
this.props.onCancel();
|
||||
this.refInput.blur();
|
||||
};
|
||||
|
||||
getValue = () => this.refInput.value;
|
||||
setValue = (value) => (this.refInput.value = value);
|
||||
|
||||
saveValue = () => {
|
||||
if (this.getValue() !== this.props.value) {
|
||||
this.props.onSave(this.getValue());
|
||||
}
|
||||
};
|
||||
|
||||
focus = () => this.refInput.focus();
|
||||
|
||||
setRef = (ref) => {
|
||||
this.refInput = ref;
|
||||
if (this.props.resize !== "none") {
|
||||
autosize(this.refInput);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { autoFocus, resize, border, autoResize, value, placeholder } = this.props;
|
||||
|
||||
return (
|
||||
<InlineInput
|
||||
style={{ resize: resize }}
|
||||
ref={this.setRef}
|
||||
border={border}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={value.length === 0 ? undefined : placeholder}
|
||||
defaultValue={value}
|
||||
rows={3}
|
||||
autoResize={autoResize}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NewLaneTitleEditor.propTypes = {
|
||||
onSave: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
border: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
autoFocus: PropTypes.bool,
|
||||
autoResize: PropTypes.bool,
|
||||
resize: PropTypes.oneOf(["none", "vertical", "horizontal"])
|
||||
};
|
||||
|
||||
NewLaneTitleEditor.defaultProps = {
|
||||
inputRef: () => {},
|
||||
onSave: () => {},
|
||||
onCancel: () => {},
|
||||
placeholder: "",
|
||||
value: "",
|
||||
border: false,
|
||||
autoFocus: false,
|
||||
autoResize: false,
|
||||
resize: "none"
|
||||
};
|
||||
|
||||
export default NewLaneTitleEditor;
|
||||
11
client/src/components/trello-board/widgets/index.jsx
Normal file
11
client/src/components/trello-board/widgets/index.jsx
Normal 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;
|
||||
@@ -45,7 +45,7 @@ if (import.meta.env.PROD) {
|
||||
maskAllText: false,
|
||||
blockAllMedia: true
|
||||
}),
|
||||
new Sentry.BrowserTracing({})
|
||||
new Sentry.browserTracingIntegration()
|
||||
],
|
||||
tracePropagationTargets: [
|
||||
"api.imex.online",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
14
client/src/redux/trello/trello.actions.js
Normal file
14
client/src/redux/trello/trello.actions.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createAction } from "redux-actions";
|
||||
|
||||
export const loadBoard = createAction("LOAD_BOARD");
|
||||
export const addLane = createAction("ADD_LANE");
|
||||
export const addCard = createAction("ADD_CARD");
|
||||
export const updateCard = createAction("UPDATE_CARD");
|
||||
export const removeCard = createAction("REMOVE_CARD");
|
||||
export const moveCardAcrossLanes = createAction("MOVE_CARD");
|
||||
export const updateCards = createAction("UPDATE_CARDS");
|
||||
export const updateLanes = createAction("UPDATE_LANES");
|
||||
export const updateLane = createAction("UPDATE_LANE");
|
||||
export const paginateLane = createAction("PAGINATE_LANE");
|
||||
export const moveLane = createAction("MOVE_LANE");
|
||||
export const removeLane = createAction("REMOVE_LANE");
|
||||
35
client/src/redux/trello/trello.reducer.js
Normal file
35
client/src/redux/trello/trello.reducer.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import Lh from "../../components/trello-board/helpers/LaneHelper";
|
||||
|
||||
const boardReducer = (state = { lanes: [] }, action) => {
|
||||
const { payload, type } = action;
|
||||
switch (type) {
|
||||
case "LOAD_BOARD":
|
||||
return Lh.initialiseLanes(state, payload);
|
||||
case "ADD_CARD":
|
||||
return Lh.appendCardToLane(state, payload);
|
||||
case "REMOVE_CARD":
|
||||
return Lh.removeCardFromLane(state, payload);
|
||||
case "MOVE_CARD":
|
||||
return Lh.moveCardAcrossLanes(state, payload);
|
||||
case "UPDATE_CARDS":
|
||||
return Lh.updateCardsForLane(state, payload);
|
||||
case "UPDATE_CARD":
|
||||
return Lh.updateCardForLane(state, payload);
|
||||
case "UPDATE_LANES":
|
||||
return Lh.updateLanes(state, payload);
|
||||
case "UPDATE_LANE":
|
||||
return Lh.updateLane(state, payload);
|
||||
case "PAGINATE_LANE":
|
||||
return Lh.paginateLane(state, payload);
|
||||
case "MOVE_LANE":
|
||||
return Lh.moveLane(state, payload);
|
||||
case "REMOVE_LANE":
|
||||
return Lh.removeLane(state, payload);
|
||||
case "ADD_LANE":
|
||||
return Lh.addLane(state, payload);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default boardReducer;
|
||||
@@ -3460,6 +3460,18 @@
|
||||
"validation": {
|
||||
"unique_vendor_name": "You must enter a unique vendor name."
|
||||
}
|
||||
}
|
||||
},
|
||||
"trello": {
|
||||
"labels": {
|
||||
"add_card": "Add Card",
|
||||
"add_lane": "Add Lane",
|
||||
"delete_lane": "Delete Lane",
|
||||
"lane_actions": "Lane Actions",
|
||||
"title": "Title",
|
||||
"description": "Description",
|
||||
"label": "Label",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3460,6 +3460,18 @@
|
||||
"validation": {
|
||||
"unique_vendor_name": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"trello": {
|
||||
"labels": {
|
||||
"add_card": "",
|
||||
"add_lane": "",
|
||||
"delete_lane": "",
|
||||
"lane_actions": "",
|
||||
"title": "",
|
||||
"description": "",
|
||||
"label": "",
|
||||
"cancel": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3460,6 +3460,18 @@
|
||||
"validation": {
|
||||
"unique_vendor_name": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"trello": {
|
||||
"labels": {
|
||||
"add_card": "",
|
||||
"add_lane": "",
|
||||
"delete_lane": "",
|
||||
"lane_actions": "",
|
||||
"title": "",
|
||||
"description": "",
|
||||
"label": "",
|
||||
"cancel": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as path from "path";
|
||||
import * as url from "url";
|
||||
import { defineConfig } from "vite";
|
||||
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||
import eslint from 'vite-plugin-eslint';
|
||||
import eslint from "vite-plugin-eslint";
|
||||
|
||||
//import CompressionPlugin from 'vite-plugin-compression';
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
@@ -103,7 +103,7 @@ export default defineConfig({
|
||||
}),
|
||||
reactVirtualized(),
|
||||
react(),
|
||||
eslint(),
|
||||
eslint()
|
||||
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.
|
||||
],
|
||||
define: {
|
||||
|
||||
2595
package-lock.json
generated
2595
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -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",
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user