Compare commits
97 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cf4a50a83 | ||
|
|
d846894d22 | ||
|
|
1c2be3c890 | ||
|
|
05e4eacf34 | ||
|
|
6dc03d7f67 | ||
|
|
2a5e6a51aa | ||
|
|
885477fa68 | ||
|
|
8f031a78c1 | ||
|
|
c7db792793 | ||
|
|
31be51ef79 | ||
|
|
e4066c1570 | ||
|
|
69b36a4c34 | ||
|
|
c7b8df5655 | ||
|
|
d85768b2ac | ||
|
|
ec480f2cb1 | ||
|
|
a569c1f4f9 | ||
|
|
f5b30c9376 | ||
|
|
412508fbcf | ||
|
|
dbfd9bce54 | ||
|
|
07a8e5b216 | ||
|
|
38bf58c613 | ||
|
|
ba90d72d55 | ||
|
|
9889bee924 | ||
|
|
a19e4e8f16 | ||
|
|
5121852fbc | ||
|
|
52e0558c79 | ||
|
|
ec00697d31 | ||
|
|
c25714b68e | ||
|
|
562d0b8641 | ||
|
|
dc0147c5f9 | ||
|
|
296afdbeee | ||
|
|
d48d6cdd91 | ||
|
|
9363790541 | ||
|
|
2bceba948d | ||
|
|
2f8f058c5c | ||
|
|
168d4246af | ||
|
|
cdf6050ec2 | ||
|
|
68784018e6 | ||
|
|
19dfec2a34 | ||
|
|
55d729339f | ||
|
|
c3108a17f4 | ||
|
|
d47ae64bd6 | ||
|
|
f451155689 | ||
|
|
9f06e19346 | ||
|
|
f4be3e9668 | ||
|
|
095e1e9789 | ||
|
|
a0b9f99dd3 | ||
|
|
a33a92207b | ||
|
|
ee613db0cb | ||
|
|
55b892a74e | ||
|
|
16754d9657 | ||
|
|
33db67122c | ||
|
|
b9b88b0e23 | ||
|
|
992ad71910 | ||
|
|
51d1f926c2 | ||
|
|
c70447f337 | ||
|
|
dc367e1a30 | ||
|
|
f647e1ff11 | ||
|
|
ee7997ffbc | ||
|
|
2dbb5adbbb | ||
|
|
cc9f342575 | ||
|
|
252262f4a7 | ||
|
|
f77a16648f | ||
|
|
d6e3c54b68 | ||
|
|
7294e96a77 | ||
|
|
a4f4f45251 | ||
|
|
acc91abc0c | ||
|
|
18d5176cb9 | ||
|
|
d6d6ced7a4 | ||
|
|
52809cc849 | ||
|
|
c525b7ea3f | ||
|
|
e799417aaf | ||
|
|
ef077c2d48 | ||
|
|
e78b114544 | ||
|
|
df170ddd27 | ||
|
|
17928741e3 | ||
|
|
a6b825ffdf | ||
|
|
0d3bc0e95f | ||
|
|
0f62a2d73d | ||
|
|
bcdd32f92f | ||
|
|
6865fcbffc | ||
|
|
8e623c71a9 | ||
|
|
1e06502464 | ||
|
|
f2914868e2 | ||
|
|
561f0313f9 | ||
|
|
1e27c4e1ae | ||
|
|
02a49efbea | ||
|
|
5f2a5e1025 | ||
|
|
42552e4f4f | ||
|
|
f2af78f056 | ||
|
|
269d55bde3 | ||
|
|
71f3dbbeb4 | ||
|
|
73dfb74ed7 | ||
|
|
7dd6baef33 | ||
|
|
519b532091 | ||
|
|
4fb9c37c0d | ||
|
|
c9b63be29f |
@@ -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"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
56
client/public/firebase-messaging-sw.js
Normal file
56
client/public/firebase-messaging-sw.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// Scripts for firebase and firebase messaging
|
||||
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js");
|
||||
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js");
|
||||
|
||||
// Initialize the Firebase app in the service worker by passing the generated config
|
||||
let firebaseConfig;
|
||||
switch (this.location.hostname) {
|
||||
case "localhost":
|
||||
firebaseConfig = {
|
||||
apiKey: "AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc",
|
||||
authDomain: "imex-dev.firebaseapp.com",
|
||||
databaseURL: "https://imex-dev.firebaseio.com",
|
||||
projectId: "imex-dev",
|
||||
storageBucket: "imex-dev.appspot.com",
|
||||
messagingSenderId: "759548147434",
|
||||
appId: "1:759548147434:web:e8239868a48ceb36700993",
|
||||
measurementId: "G-K5XRBVVB4S",
|
||||
};
|
||||
break;
|
||||
case "test.imex.online":
|
||||
firebaseConfig = {
|
||||
apiKey: "AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c",
|
||||
authDomain: "imex-test.firebaseapp.com",
|
||||
projectId: "imex-test",
|
||||
storageBucket: "imex-test.appspot.com",
|
||||
messagingSenderId: "991923618608",
|
||||
appId: "1:991923618608:web:633437569cdad78299bef5",
|
||||
// measurementId: "${config.measurementId}",
|
||||
};
|
||||
break;
|
||||
case "imex.online":
|
||||
default:
|
||||
firebaseConfig = {
|
||||
apiKey: "AIzaSyDSezy-jGJreo7ulgpLdlpOwAOrgcaEkhU",
|
||||
authDomain: "imex-prod.firebaseapp.com",
|
||||
databaseURL: "https://imex-prod.firebaseio.com",
|
||||
projectId: "imex-prod",
|
||||
storageBucket: "imex-prod.appspot.com",
|
||||
messagingSenderId: "253497221485",
|
||||
appId: "1:253497221485:web:3c81c483b94db84b227a64",
|
||||
measurementId: "G-NTWBKG2L0M",
|
||||
};
|
||||
}
|
||||
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
|
||||
// Retrieve firebase messaging
|
||||
const messaging = firebase.messaging();
|
||||
|
||||
messaging.onBackgroundMessage(function (payload) {
|
||||
// Customize notification here
|
||||
const channel = new BroadcastChannel("imex-sw-messages");
|
||||
channel.postMessage(payload);
|
||||
|
||||
//self.registration.showNotification(notificationTitle, notificationOptions);
|
||||
});
|
||||
@@ -150,7 +150,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
<ProductFruits
|
||||
workspaceCode={InstanceRenderMgr({
|
||||
imex: null,
|
||||
rome: null,
|
||||
rome: "9BkbEseqNqxw8jUH",
|
||||
promanager: "aoJoEifvezYI0Z0P"
|
||||
})}
|
||||
debug
|
||||
|
||||
@@ -25,31 +25,27 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
||||
);
|
||||
}}
|
||||
notFoundContent={"Removed."}
|
||||
{...restProps}
|
||||
>
|
||||
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
||||
{t("billlines.labels.other")}
|
||||
</Select.Option>
|
||||
{options
|
||||
? options.map((item) => (
|
||||
<Option
|
||||
disabled={allowRemoved ? false : item.removed}
|
||||
key={item.id}
|
||||
value={item.id}
|
||||
cost={item.act_price ? item.act_price : 0}
|
||||
part_type={item.part_type}
|
||||
line_desc={item.line_desc}
|
||||
part_qty={item.part_qty}
|
||||
oem_partno={item.oem_partno}
|
||||
alt_partno={item.alt_partno}
|
||||
act_price={item.act_price}
|
||||
style={{
|
||||
...(item.removed ? { textDecoration: "line-through" } : {})
|
||||
}}
|
||||
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
|
||||
>
|
||||
options={[
|
||||
{ value: "noline", label: t("billlines.labels.other"), name: t("billlines.labels.other") },
|
||||
...options.map((item) => ({
|
||||
disabled: allowRemoved ? false : item.removed,
|
||||
key: item.id,
|
||||
value: item.id,
|
||||
cost: item.act_price ? item.act_price : 0,
|
||||
part_type: item.part_type,
|
||||
line_desc: item.line_desc,
|
||||
part_qty: item.part_qty,
|
||||
oem_partno: item.oem_partno,
|
||||
alt_partno: item.alt_partno,
|
||||
act_price: item.act_price,
|
||||
style: {
|
||||
...(item.removed ? { textDecoration: "line-through" } : {})
|
||||
},
|
||||
name: `${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
|
||||
label: (
|
||||
<>
|
||||
<span>
|
||||
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
@@ -60,14 +56,15 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
||||
<span style={{ float: "right", paddingleft: "1rem" }}>{`${item.mod_lb_hrs} units`}</span>
|
||||
)
|
||||
})}
|
||||
|
||||
<span style={{ float: "right", paddingleft: "1rem" }}>
|
||||
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
|
||||
</span>
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
</>
|
||||
)
|
||||
}))
|
||||
]}
|
||||
{...restProps}
|
||||
></Select>
|
||||
);
|
||||
};
|
||||
export default forwardRef(BillLineSearchSelect);
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic } from "antd";
|
||||
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
|
||||
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
||||
@@ -28,12 +26,12 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
|
||||
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
|
||||
const { context } = cardPaymentModal;
|
||||
const { context, actions } = cardPaymentModal;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -42,7 +40,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
skip: true
|
||||
});
|
||||
|
||||
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
|
||||
//Initialize the intellipay window.
|
||||
const SetIntellipayCallbackFunctions = () => {
|
||||
console.log("*** Set IntelliPay callback functions.");
|
||||
@@ -51,16 +48,20 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
});
|
||||
|
||||
window.intellipay.runOnApproval(async function (response) {
|
||||
console.warn("*** Running On Approval Script ***");
|
||||
form.setFieldValue("paymentResponse", response);
|
||||
form.submit();
|
||||
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
|
||||
//Add a slight delay to allow the refetch to properly get the data.
|
||||
setTimeout(() => {
|
||||
if (actions && actions.refetch && typeof actions.refetch === "function")
|
||||
actions.refetch();
|
||||
setLoading(false);
|
||||
toggleModalVisible();
|
||||
}, 750);
|
||||
});
|
||||
|
||||
window.intellipay.runOnNonApproval(async function (response) {
|
||||
// Mutate unsuccessful payment
|
||||
|
||||
const { payments } = form.getFieldsValue();
|
||||
|
||||
await insertPaymentResponse({
|
||||
variables: {
|
||||
paymentResponse: payments.map((payment) => ({
|
||||
@@ -85,50 +86,9 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
});
|
||||
};
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
try {
|
||||
await insertPayment({
|
||||
variables: {
|
||||
paymentInput: values.payments.map((payment) => ({
|
||||
amount: payment.amount,
|
||||
transactionid: (values.paymentResponse.paymentid || "").toString(),
|
||||
payer: t("payments.labels.customer"),
|
||||
type: values.paymentResponse.cardbrand,
|
||||
jobid: payment.jobid,
|
||||
date: dayjs(Date.now()),
|
||||
payment_responses: {
|
||||
data: [
|
||||
{
|
||||
amount: payment.amount,
|
||||
bodyshopid: bodyshop.id,
|
||||
|
||||
jobid: payment.jobid,
|
||||
declinereason: values.paymentResponse.declinereason,
|
||||
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
||||
successful: true,
|
||||
response: values.paymentResponse
|
||||
}
|
||||
]
|
||||
}
|
||||
}))
|
||||
},
|
||||
refetchQueries: ["GET_JOB_BY_PK"]
|
||||
});
|
||||
toggleModalVisible();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("payments.errors.inserting", { error: error.message })
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIntelliPayCharge = async () => {
|
||||
setLoading(true);
|
||||
|
||||
//Validate
|
||||
try {
|
||||
await form.validateFields();
|
||||
@@ -140,7 +100,8 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
try {
|
||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||
bodyshop,
|
||||
refresh: !!window.intellipay
|
||||
refresh: !!window.intellipay,
|
||||
paymentSplitMeta: form.getFieldsValue(),
|
||||
});
|
||||
|
||||
if (window.intellipay) {
|
||||
@@ -169,7 +130,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
<Card title="Card Payment">
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
@@ -246,18 +206,14 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
console.log("Updating the owner info section.");
|
||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||
const { payments } = form.getFieldsValue();
|
||||
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
|
||||
console.log("**Calling refetch.");
|
||||
if (
|
||||
payments?.length > 0 &&
|
||||
payments?.filter((p) => p?.jobid).length === payments?.length
|
||||
) {
|
||||
refetch({ jobids: payments.map((p) => p.jobid) });
|
||||
}
|
||||
console.log(
|
||||
"Acc info",
|
||||
data,
|
||||
payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
@@ -300,6 +256,13 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
type="hidden"
|
||||
value={totalAmountToCharge?.toFixed(2)}
|
||||
/>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="comment"
|
||||
type="hidden"
|
||||
value={btoa(JSON.stringify(payments))}
|
||||
hidden
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
// data-ipayname="submit"
|
||||
@@ -314,11 +277,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* Lightbox payment response when it is completed */}
|
||||
<Form.Item name="paymentResponse" hidden>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
@@ -28,6 +28,7 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
|
||||
<Option value="courtesycars.status.out">{t("courtesycars.status.out")}</Option>
|
||||
<Option value="courtesycars.status.sold">{t("courtesycars.status.sold")}</Option>
|
||||
<Option value="courtesycars.status.leasereturn">{t("courtesycars.status.leasereturn")}</Option>
|
||||
<Option value="courtesycars.status.unavailable">{t("courtesycars.status.unavailable")}</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -61,7 +61,11 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
{
|
||||
text: t("courtesycars.status.leasereturn"),
|
||||
value: "courtesycars.status.leasereturn"
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.status.unavailable"),
|
||||
value: "courtesycars.status.unavailable",
|
||||
},
|
||||
],
|
||||
onFilter: (value, record) => record.status === value,
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -3,14 +3,13 @@ import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
|
||||
export default function GlobalSearchOs() {
|
||||
const { t } = useTranslation();
|
||||
const history = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState(false);
|
||||
|
||||
@@ -178,15 +177,7 @@ export default function GlobalSearchOs() {
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
options={data}
|
||||
onSearch={handleSearch}
|
||||
defaultActiveFirstOption
|
||||
onSelect={(val, opt) => {
|
||||
history(opt.label.props.to);
|
||||
}}
|
||||
onClear={() => setData([])}
|
||||
>
|
||||
<AutoComplete options={data} onSearch={handleSearch} defaultActiveFirstOption onClear={() => setData([])}>
|
||||
<Input.Search
|
||||
size="large"
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AutoComplete, Divider, Input, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
@@ -12,7 +12,6 @@ import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.compon
|
||||
|
||||
export default function GlobalSearch() {
|
||||
const { t } = useTranslation();
|
||||
const history = useNavigate();
|
||||
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
|
||||
|
||||
const executeSearch = (v) => {
|
||||
@@ -157,14 +156,7 @@ export default function GlobalSearch() {
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
options={options}
|
||||
onSearch={handleSearch}
|
||||
defaultActiveFirstOption
|
||||
onSelect={(val, opt) => {
|
||||
history(opt.label.props.to);
|
||||
}}
|
||||
>
|
||||
<AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption>
|
||||
<Input.Search
|
||||
size="large"
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
|
||||
@@ -32,6 +32,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsKanban } from "react-icons/bs";
|
||||
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||
import { IoBusinessOutline } from "react-icons/io5";
|
||||
import { RiSurveyLine } from "react-icons/ri";
|
||||
@@ -42,7 +43,6 @@ import { selectRecentItems, selectSelectedHeader } from "../../redux/application
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
import { checkBeta, handleBeta, setBeta } from "../../utils/betaHandler";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
@@ -141,11 +141,13 @@ function Header({
|
||||
accountingChildren.push(
|
||||
{
|
||||
key: "bills",
|
||||
id: "header-accounting-bills",
|
||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
||||
label: <Link to="/manage/bills">{t("menus.header.bills")}</Link>
|
||||
},
|
||||
{
|
||||
key: "enterbills",
|
||||
id: "header-accounting-enterbills",
|
||||
icon: <Icon component={GiPayMoney} />,
|
||||
label: t("menus.header.enterbills"),
|
||||
onClick: () => {
|
||||
@@ -165,6 +167,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "inventory",
|
||||
id: "header-accounting-inventory",
|
||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
||||
label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
|
||||
}
|
||||
@@ -183,11 +186,13 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "allpayments",
|
||||
id: "header-accounting-allpayments",
|
||||
icon: <BankFilled />,
|
||||
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||
},
|
||||
{
|
||||
key: "enterpayments",
|
||||
id: "header-accounting-enterpayments",
|
||||
icon: <Icon component={FaCreditCard} />,
|
||||
label: t("menus.header.enterpayment"),
|
||||
onClick: () => {
|
||||
@@ -203,6 +208,7 @@ function Header({
|
||||
if (ImEXPay.treatment === "on") {
|
||||
accountingChildren.push({
|
||||
key: "entercardpayments",
|
||||
id: "header-accounting-entercardpayments",
|
||||
icon: <Icon component={FaCreditCard} />,
|
||||
label: t("menus.header.entercardpayment"),
|
||||
onClick: () => {
|
||||
@@ -227,6 +233,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "timetickets",
|
||||
id: "header-accounting-timetickets",
|
||||
icon: <FieldTimeOutlined />,
|
||||
label: <Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link>
|
||||
}
|
||||
@@ -235,6 +242,7 @@ function Header({
|
||||
if (bodyshop?.md_tasks_presets?.use_approvals) {
|
||||
accountingChildren.push({
|
||||
key: "ttapprovals",
|
||||
id: "header-accounting-ttapprovals",
|
||||
icon: <FieldTimeOutlined />,
|
||||
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
|
||||
});
|
||||
@@ -244,6 +252,7 @@ function Header({
|
||||
key: "entertimetickets",
|
||||
icon: <Icon component={GiPlayerTime} />,
|
||||
label: t("menus.header.entertimeticket"),
|
||||
id: "header-accounting-entertimetickets",
|
||||
onClick: () => {
|
||||
setTimeTicketContext({
|
||||
actions: {},
|
||||
@@ -264,6 +273,7 @@ function Header({
|
||||
const accountingExportChildren = [
|
||||
{
|
||||
key: "receivables",
|
||||
id: "header-accounting-receivables",
|
||||
label: <Link to="/manage/accounting/receivables">{t("menus.header.accounting-receivables")}</Link>
|
||||
}
|
||||
];
|
||||
@@ -271,6 +281,7 @@ function Header({
|
||||
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on") {
|
||||
accountingExportChildren.push({
|
||||
key: "payables",
|
||||
id: "header-accounting-payables",
|
||||
label: <Link to="/manage/accounting/payables">{t("menus.header.accounting-payables")}</Link>
|
||||
});
|
||||
}
|
||||
@@ -278,6 +289,7 @@ function Header({
|
||||
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))) {
|
||||
accountingExportChildren.push({
|
||||
key: "payments",
|
||||
id: "header-accounting-payments",
|
||||
label: <Link to="/manage/accounting/payments">{t("menus.header.accounting-payments")}</Link>
|
||||
});
|
||||
}
|
||||
@@ -288,6 +300,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "exportlogs",
|
||||
id: "header-accounting-exportlogs",
|
||||
label: <Link to="/manage/accounting/exportlogs">{t("menus.header.export-logs")}</Link>
|
||||
}
|
||||
);
|
||||
@@ -301,6 +314,7 @@ function Header({
|
||||
) {
|
||||
accountingChildren.push({
|
||||
key: "accountingexport",
|
||||
id: "header-accounting-export",
|
||||
icon: <ExportOutlined />,
|
||||
label: t("menus.header.export"),
|
||||
children: accountingExportChildren
|
||||
@@ -604,14 +618,22 @@ function Header({
|
||||
);
|
||||
}
|
||||
},
|
||||
// {
|
||||
// key: 'rescue',
|
||||
// icon: <Icon component={CarFilled}/>,
|
||||
// label: t("menus.header.rescueme"),
|
||||
// onClick: () => {
|
||||
// window.open("https://imexrescue.com/", "_blank");
|
||||
// }
|
||||
// },
|
||||
...(InstanceRenderManager({
|
||||
imex: true,
|
||||
rome: false,
|
||||
promanager: false
|
||||
})
|
||||
? [
|
||||
{
|
||||
key: "rescue",
|
||||
icon: <Icon component={CarFilled} />,
|
||||
label: t("menus.header.rescueme"),
|
||||
onClick: () => {
|
||||
window.open("https://imexrescue.com/", "_blank");
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
|
||||
...(InstanceRenderManager({
|
||||
imex: true,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { QUERY_JOBLINE_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
||||
import TaskListContainer from "../task-list/task-list.container.jsx";
|
||||
import FeatureWrapper from "../feature-wrapper/feature-wrapper.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -98,55 +99,59 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: {
|
||||
key: "dispatch-lines",
|
||||
children: t("parts_orders.labels.notyetordered")
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={24} lg={8}>
|
||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||
<Timeline
|
||||
items={
|
||||
data.billlines.length > 0
|
||||
? data.billlines.map((line) => ({
|
||||
key: line.id,
|
||||
children: (
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||
{line.bill.invoice_number}
|
||||
</Link>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_price")}: `}
|
||||
<CurrencyFormatter>{line.actual_price}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_cost")}: `}
|
||||
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<DateFormatter>{line.bill.date}</DateFormatter>
|
||||
</Col>
|
||||
<Col span={4}> {line.bill.vendor.name}</Col>
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: "no-orders",
|
||||
children: t("bills.labels.nobilllines")
|
||||
key: "dispatch-lines",
|
||||
children: t("parts_dispatch.labels.notyetdispatched")
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<FeatureWrapper featureName="bills" noauth={() => null}>
|
||||
<Col md={24} lg={8}>
|
||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||
<Timeline
|
||||
items={
|
||||
data.billlines.length > 0
|
||||
? data.billlines.map((line) => ({
|
||||
key: line.id,
|
||||
children: (
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||
{line.bill.invoice_number}
|
||||
</Link>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_price")}: `}
|
||||
<CurrencyFormatter>{line.actual_price}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_cost")}: `}
|
||||
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<DateFormatter>{line.bill.date}</DateFormatter>
|
||||
</Col>
|
||||
<Col span={4}> {line.bill.vendor.name}</Col>
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: "no-orders",
|
||||
children: t("bills.labels.nobilllines")
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</FeatureWrapper>
|
||||
<Col md={24} lg={24}>
|
||||
<TaskListContainer
|
||||
parentJobId={jobid}
|
||||
|
||||
@@ -43,6 +43,7 @@ import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.con
|
||||
import JobLinesExpander from "./job-lines-expander.component";
|
||||
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -552,7 +553,7 @@ export function JobLinesComponent({
|
||||
>
|
||||
{t("joblines.actions.new")}
|
||||
</Button>
|
||||
{bodyshop.region_config.toLowerCase().startsWith("us") && <JobSendPartPriceChangeComponent job={job} />}
|
||||
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} /> })}
|
||||
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Card, Dropdown, Form, Input, Modal, Popconfirm, Popover, Select, Space, notification } from "antd";
|
||||
import { Button, Card, Dropdown, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import React, { useMemo, useState } from "react";
|
||||
@@ -641,6 +641,7 @@ export function JobsDetailHeaderActions({
|
||||
const menuItems = [
|
||||
{
|
||||
key: "schedule",
|
||||
id: "job-actions-schedule",
|
||||
disabled: !jobInPreProduction || !job.converted || jobRO,
|
||||
label: t("jobs.actions.schedule"),
|
||||
onClick: () => {
|
||||
@@ -657,6 +658,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "cancelallappointments",
|
||||
id: "job-actions-cancelallappointments",
|
||||
onClick: () => {
|
||||
if (job.status !== bodyshop.md_ro_statuses.default_scheduled) {
|
||||
return;
|
||||
@@ -670,6 +672,7 @@ export function JobsDetailHeaderActions({
|
||||
imex: [
|
||||
{
|
||||
key: "intake",
|
||||
id: "job-actions-intake",
|
||||
disabled: !!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO,
|
||||
label:
|
||||
!!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO ? (
|
||||
@@ -680,6 +683,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "deliver",
|
||||
id: "job-actions-deliver",
|
||||
disabled: !jobInProduction || jobRO,
|
||||
label: !jobInProduction ? (
|
||||
t("jobs.actions.deliver")
|
||||
@@ -689,6 +693,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "checklist",
|
||||
id: "job-actions-checklist",
|
||||
disabled: !job.converted,
|
||||
label: <Link to={`/manage/jobs/${job.id}/checklist`}>{t("jobs.actions.viewchecklist")}</Link>
|
||||
}
|
||||
@@ -697,6 +702,7 @@ export function JobsDetailHeaderActions({
|
||||
promanager: [
|
||||
{
|
||||
key: "toggleproduction",
|
||||
id: "job-actions-toggleproduction",
|
||||
disabled: !job.converted || jobRO,
|
||||
label: <JobsDetailHeaderActionsToggleProduction job={job} refetch={refetch} />
|
||||
}
|
||||
@@ -710,6 +716,7 @@ export function JobsDetailHeaderActions({
|
||||
? [
|
||||
{
|
||||
key: "entertimetickets",
|
||||
id: "job-actions-entertimetickets",
|
||||
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
||||
label: t("timetickets.actions.enter"),
|
||||
onClick: () => {
|
||||
@@ -733,6 +740,7 @@ export function JobsDetailHeaderActions({
|
||||
if (bodyshop.md_tasks_presets.enable_tasks) {
|
||||
menuItems.push({
|
||||
key: "claimtimetickettasks",
|
||||
id: "job-actions-claimtimetickettasks",
|
||||
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
||||
onClick: () => {
|
||||
setTimeTicketTaskContext({
|
||||
@@ -746,6 +754,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
menuItems.push({
|
||||
key: "enterpayments",
|
||||
id: "job-actions-enterpayments",
|
||||
disabled: !job.converted,
|
||||
label: t("menus.header.enterpayment"),
|
||||
onClick: () => {
|
||||
@@ -761,13 +770,14 @@ export function JobsDetailHeaderActions({
|
||||
if (ImEXPay.treatment === "on") {
|
||||
menuItems.push({
|
||||
key: "entercardpayments",
|
||||
id: "job-actions-entercardpayments",
|
||||
disabled: !job.converted,
|
||||
label: t("menus.header.entercardpayment"),
|
||||
onClick: () => {
|
||||
logImEXEvent("job_header_enter_card_payment");
|
||||
|
||||
setCardPaymentContext({
|
||||
actions: {},
|
||||
actions: { refetch },
|
||||
context: { jobid: job.id }
|
||||
});
|
||||
}
|
||||
@@ -777,6 +787,7 @@ export function JobsDetailHeaderActions({
|
||||
if (HasFeatureAccess({ featureName: "courtesycars", bodyshop })) {
|
||||
menuItems.push({
|
||||
key: "cccontract",
|
||||
id: "job-actions-cccontract",
|
||||
disabled: jobRO || !job.converted,
|
||||
label: (
|
||||
<Link state={{ jobId: job.id }} to="/manage/courtesycars/contracts/new">
|
||||
@@ -788,6 +799,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
menuItems.push({
|
||||
key: "createtask",
|
||||
id: "job-actions-createtask",
|
||||
label: t("menus.header.create_task"),
|
||||
onClick: () =>
|
||||
setTaskUpsertContext({
|
||||
@@ -800,12 +812,14 @@ export function JobsDetailHeaderActions({
|
||||
job.inproduction
|
||||
? {
|
||||
key: "removefromproduction",
|
||||
id: "job-actions-removefromproduction",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.removefromproduction"),
|
||||
onClick: () => AddToProduction(client, job.id, refetch, true)
|
||||
}
|
||||
: {
|
||||
key: "addtoproduction",
|
||||
id: "job-actions-addtoproduction",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.addtoproduction"),
|
||||
onClick: () => AddToProduction(client, job.id, refetch)
|
||||
@@ -815,12 +829,14 @@ export function JobsDetailHeaderActions({
|
||||
menuItems.push(
|
||||
{
|
||||
key: "togglesuspend",
|
||||
id: "job-actions-togglesuspend",
|
||||
onClick: handleSuspend,
|
||||
label: job.suspended ? t("production.actions.unsuspend") : t("production.actions.suspend")
|
||||
},
|
||||
{
|
||||
key: "toggleAlert",
|
||||
onClick: handleAlertToggle,
|
||||
id: "job-actions-togglealert",
|
||||
label:
|
||||
job.production_vars && job.production_vars.alert
|
||||
? t("production.labels.alertoff")
|
||||
@@ -832,6 +848,7 @@ export function JobsDetailHeaderActions({
|
||||
children: [
|
||||
{
|
||||
key: "duplicate",
|
||||
id: "job-actions-duplicate",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
@@ -847,6 +864,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "duplicatenolines",
|
||||
id: "job-actions-duplicatenolines",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
@@ -870,6 +888,7 @@ export function JobsDetailHeaderActions({
|
||||
? [
|
||||
{
|
||||
key: "postbills",
|
||||
id: "job-actions-postbills",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.postbills"),
|
||||
onClick: () => {
|
||||
@@ -888,6 +907,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
{
|
||||
key: "addtopartsqueue",
|
||||
id: "job-actions-addtopartsqueue",
|
||||
disabled: !job.converted || !jobInProduction || jobRO,
|
||||
label: t("jobs.actions.addtopartsqueue"),
|
||||
onClick: async () => {
|
||||
@@ -913,6 +933,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "closejob",
|
||||
id: "job-actions-closejob",
|
||||
disabled: !jobInPostProduction,
|
||||
label: !jobInPostProduction ? (
|
||||
t("menus.jobsactions.closejob")
|
||||
@@ -928,6 +949,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "admin",
|
||||
id: "job-actions-admin",
|
||||
label: (
|
||||
<Link
|
||||
to={{
|
||||
@@ -949,6 +971,7 @@ export function JobsDetailHeaderActions({
|
||||
) {
|
||||
menuItems.push({
|
||||
key: "exportcustdata",
|
||||
id: "job-actions-exportcustdata",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.exportcustdata"),
|
||||
onClick: handleExportCustData
|
||||
@@ -959,18 +982,21 @@ export function JobsDetailHeaderActions({
|
||||
const children = [
|
||||
{
|
||||
key: "email",
|
||||
id: "job-actions-email",
|
||||
disabled: !!!job.ownr_ea,
|
||||
label: t("general.labels.email"),
|
||||
onClick: handleCreateCsi
|
||||
},
|
||||
{
|
||||
key: "text",
|
||||
id: "job-actions-text",
|
||||
disabled: !!!job.ownr_ph1,
|
||||
label: t("general.labels.text"),
|
||||
onClick: handleCreateCsi
|
||||
},
|
||||
{
|
||||
key: "generate",
|
||||
id: "job-actions-generate",
|
||||
disabled: job.csiinvites && job.csiinvites.length > 0,
|
||||
label: t("jobs.actions.generatecsi"),
|
||||
onClick: handleCreateCsi
|
||||
@@ -1004,6 +1030,7 @@ export function JobsDetailHeaderActions({
|
||||
}
|
||||
menuItems.push({
|
||||
key: "sendcsi",
|
||||
id: "job-actions-sendcsi",
|
||||
label: t("jobs.actions.sendcsi"),
|
||||
disabled: !job.converted,
|
||||
children
|
||||
@@ -1012,6 +1039,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
menuItems.push({
|
||||
key: "jobcosting",
|
||||
id: "job-actions-jobcosting",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.labels.jobcosting"),
|
||||
onClick: () => {
|
||||
@@ -1029,6 +1057,7 @@ export function JobsDetailHeaderActions({
|
||||
if (job && !job.converted) {
|
||||
menuItems.push({
|
||||
key: "deletejob",
|
||||
id: "job-actions-deletejob",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.deleteconfirm")}
|
||||
@@ -1045,6 +1074,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
menuItems.push({
|
||||
key: "manualevent",
|
||||
id: "job-actions-manualevent",
|
||||
onClick: (e) => {
|
||||
setVisibility(true);
|
||||
},
|
||||
@@ -1054,6 +1084,7 @@ export function JobsDetailHeaderActions({
|
||||
if (!jobRO && job.converted) {
|
||||
menuItems.push({
|
||||
key: "voidjob",
|
||||
id: "job-actions-voidjob",
|
||||
label: (
|
||||
<RbacWrapper action="jobs:void" noauth>
|
||||
<Popconfirm
|
||||
|
||||
@@ -18,7 +18,6 @@ import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { setJoyRideSteps } from "../../redux/application/application.actions";
|
||||
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -315,34 +314,6 @@ export function JobsList({ bodyshop, setJoyRideSteps }) {
|
||||
title={t("titles.bc.jobs-active")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
{InstanceRenderManager({
|
||||
promanager: (
|
||||
<Button
|
||||
onClick={() =>
|
||||
setJoyRideSteps([
|
||||
{
|
||||
target: "#active-jobs-list",
|
||||
content: "This is where you will see all work coming in and currently here."
|
||||
},
|
||||
{
|
||||
target: "#header-jobs",
|
||||
spotlightClicks: true,
|
||||
disableOverlayClose: true,
|
||||
content:
|
||||
"The jobs menu lets you access additional pages to see more information. You can import new jobs, search all jobs, or manage your current production."
|
||||
},
|
||||
{
|
||||
target: "#header-jobs-available",
|
||||
content: "You can find jobs to import here."
|
||||
}
|
||||
])
|
||||
}
|
||||
>
|
||||
Start Walk Through
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
|
||||
@@ -6,7 +6,8 @@ import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -75,7 +76,18 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
||||
})),
|
||||
onFilter: (value, record) => value.includes(record.status)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.actual_completion"),
|
||||
dataIndex: "actual_completion",
|
||||
key: "actual_completion",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.actual_completion}</DateTimeFormatter>
|
||||
),
|
||||
sorter: (a, b) => dateSort(a.actual_completion, b.actual_completion),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "actual_completion" &&
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_total"),
|
||||
dataIndex: "clm_total",
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Card, Checkbox, Drawer, Grid, Input, Popconfirm, Space, Table } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
|
||||
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
|
||||
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
|
||||
@@ -81,19 +82,46 @@ export function PartsOrderListTableComponent({
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {}
|
||||
});
|
||||
|
||||
const [returnfrombill, setReturnFromBill] = useState();
|
||||
const [billData, setBillData] = useState();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const selectedpartsorder = search.partsorderid;
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
|
||||
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||
|
||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||
const { refetch } = billsQuery;
|
||||
|
||||
useEffect(() => {
|
||||
if (returnfrombill === null) {
|
||||
setBillData(null);
|
||||
} else {
|
||||
const fetchData = async () => {
|
||||
const result = await billQuery({
|
||||
variables: { billid: returnfrombill }
|
||||
});
|
||||
setBillData(result.data);
|
||||
};
|
||||
fetchData();
|
||||
}
|
||||
}, [returnfrombill, billQuery]);
|
||||
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space direction="horizontal" wrap>
|
||||
{showView && (
|
||||
<Button onClick={() => handleOnRowClick(record)}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (record.returnfrombill) {
|
||||
setReturnFromBill(record.returnfrombill);
|
||||
} else {
|
||||
setReturnFromBill(null);
|
||||
}
|
||||
handleOnRowClick(record);
|
||||
}}
|
||||
>
|
||||
<EyeFilled />
|
||||
</Button>
|
||||
)}
|
||||
@@ -175,7 +203,7 @@ export function PartsOrderListTableComponent({
|
||||
is_credit_memo: record.return,
|
||||
billlines: record.parts_order_lines.map((pol) => {
|
||||
return {
|
||||
joblineid: pol.job_line_id,
|
||||
joblineid: pol.job_line_id || "noline",
|
||||
line_desc: pol.line_desc,
|
||||
quantity: pol.quantity,
|
||||
|
||||
@@ -395,7 +423,14 @@ export function PartsOrderListTableComponent({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title={record && `${record.vendor.name} - ${record.order_number}`} extra={recordActions(record)} />
|
||||
<PageHeader
|
||||
title={
|
||||
billData
|
||||
? `${record.vendor.name} - ${record.order_number} - ${t("bills.labels.returnfrombill")}: ${billData.bills_by_pk.invoice_number}`
|
||||
: `${record.vendor.name} - ${record.order_number}`
|
||||
}
|
||||
extra={recordActions(record)}
|
||||
/>
|
||||
<Table
|
||||
scroll={{
|
||||
x: true //y: "50rem"
|
||||
|
||||
@@ -119,8 +119,14 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Descriptions title={t("job_payments.titles.descriptions")} contentStyle={{ fontWeight: "600" }} column={4}>
|
||||
<Descriptions.Item label={t("job_payments.titles.payer")}>{record.payer}</Descriptions.Item>
|
||||
<Descriptions
|
||||
title={t("job_payments.titles.descriptions")}
|
||||
contentStyle={{ fontWeight: "600" }}
|
||||
column={4}
|
||||
>
|
||||
<Descriptions.Item label={t("job_payments.titles.hint")}>
|
||||
{payment_response?.response?.methodhint}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.payername")}>
|
||||
{payment_response?.response?.nameOnCard ?? ""}
|
||||
</Descriptions.Item>
|
||||
@@ -132,7 +138,7 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.transactionid")}>{record.transactionid}</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.paymentid")}>
|
||||
{payment_response?.response?.paymentreferenceid ?? ""}
|
||||
{payment_response?.ext_paymentid ?? ""}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.paymenttype")}>{record.type}</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.paymentnum")}>{record.paymentnum}</Descriptions.Item>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -13,6 +13,7 @@ import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-forma
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
// TODO: Client Update, this might break
|
||||
const timeZonesList = Intl.supportedValuesOf("timeZone");
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -144,285 +145,289 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")} id="accountingsetup">
|
||||
<Form.Item label={t("bodyshop.labels.qbo")} valuePropName="checked" name={["accountingconfig", "qbo"]}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => (
|
||||
<FeatureWrapper featureName="export" noauth={() => null}>
|
||||
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")} id="accountingsetup">
|
||||
<Form.Item label={t("bodyshop.labels.qbo")} valuePropName="checked" name={["accountingconfig", "qbo"]}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => (
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.qbo_usa")}
|
||||
shouldUpdate
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "qbo_usa"]}
|
||||
>
|
||||
<Switch disabled={!form.getFieldValue(["accountingconfig", "qbo"])} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item label={t("bodyshop.labels.qbo_departmentid")} name={["accountingconfig", "qbo_departmentid"]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.accountingtiers")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "tiers"]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={2}>2</Radio>
|
||||
<Radio value={3}>3</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.qbo_usa")}
|
||||
label={t("bodyshop.labels.2tiersetup")}
|
||||
shouldUpdate
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "qbo_usa"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "twotierpref"]}
|
||||
>
|
||||
<Switch disabled={!form.getFieldValue(["accountingconfig", "qbo"])} />
|
||||
<Radio.Group disabled={form.getFieldValue(["accountingconfig", "tiers"]) === 3}>
|
||||
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
||||
<Radio value="source">{t("bodyshop.labels.2tiersource")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item label={t("bodyshop.labels.qbo_departmentid")} name={["accountingconfig", "qbo_departmentid"]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.accountingtiers")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "tiers"]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={2}>2</Radio>
|
||||
<Radio value={3}>3</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.printlater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "printlater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.emaillater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "emaillater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.inhousevendorid")}
|
||||
name={"inhousevendorid"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.default_adjustment_rate")}
|
||||
name={"default_adjustment_rate"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item label={t("bodyshop.fields.federal_tax_id")} name="federal_tax_id">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.2tiersetup")}
|
||||
shouldUpdate
|
||||
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
||||
name={["bill_tax_rates", "federal_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "twotierpref"]}
|
||||
>
|
||||
<Radio.Group disabled={form.getFieldValue(["accountingconfig", "tiers"]) === 3}>
|
||||
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
||||
<Radio value="source">{t("bodyshop.labels.2tiersource")}</Radio>
|
||||
</Radio.Group>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.printlater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "printlater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.emaillater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "emaillater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.inhousevendorid")}
|
||||
name={"inhousevendorid"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.default_adjustment_rate")}
|
||||
name={"default_adjustment_rate"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item label={t("bodyshop.fields.federal_tax_id")} name="federal_tax_id">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
||||
name={["bill_tax_rates", "federal_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
||||
name={["bill_tax_rates", "state_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
||||
name={["bill_tax_rates", "local_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_payment_types"]}
|
||||
label={t("bodyshop.fields.md_payment_types")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_categories"]}
|
||||
label={t("bodyshop.fields.md_categories")}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item name={["enforce_class"]} label={t("bodyshop.fields.enforce_class")} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField1"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField2"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField3"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_classes"]}
|
||||
label={t("bodyshop.fields.md_classes")}
|
||||
rules={[
|
||||
({ getFieldValue }) => {
|
||||
return {
|
||||
required: getFieldValue("enforce_class"),
|
||||
)
|
||||
})}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
||||
name={["bill_tax_rates", "state_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
||||
name={["bill_tax_rates", "local_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_payment_types"]}
|
||||
label={t("bodyshop.fields.md_payment_types")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
};
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
{ClosingPeriod.treatment === "on" && (
|
||||
<>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ClosingPeriod"]}
|
||||
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
|
||||
>
|
||||
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("bodyshop.labels.scoreboardsetup")} id="scoreboardsetup">
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.dailypainttarget")}
|
||||
name={["scoreboard_target", "dailyPaintTarget"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={0} />
|
||||
</Form.Item>
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_categories"]}
|
||||
label={t("bodyshop.fields.md_categories")}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item name={["enforce_class"]} label={t("bodyshop.fields.enforce_class")} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField1"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField2"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField3"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_classes"]}
|
||||
label={t("bodyshop.fields.md_classes")}
|
||||
rules={[
|
||||
({ getFieldValue }) => {
|
||||
return {
|
||||
required: getFieldValue("enforce_class"),
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
};
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
{ClosingPeriod.treatment === "on" && (
|
||||
<>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ClosingPeriod"]}
|
||||
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
|
||||
>
|
||||
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
</FeatureWrapper>
|
||||
<FeatureWrapper featureName="scoreboard" noauth={() => null}>
|
||||
<LayoutFormRow header={t("bodyshop.labels.scoreboardsetup")} id="scoreboardsetup">
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.dailypainttarget")}
|
||||
name={["scoreboard_target", "dailyPaintTarget"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.dailybodytarget")}
|
||||
name={["scoreboard_target", "dailyBodyTarget"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.dailybodytarget")}
|
||||
name={["scoreboard_target", "dailyBodyTarget"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.lastnumberworkingdays")}
|
||||
name={["scoreboard_target", "lastNumberWorkingDays"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={12} precision={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.ignoreblockeddays")}
|
||||
name={["scoreboard_target", "ignoreblockeddays"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.prodtargethrs")}
|
||||
name={["prodtargethrs"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} precision={1} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.lastnumberworkingdays")}
|
||||
name={["scoreboard_target", "lastNumberWorkingDays"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={12} precision={0} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.ignoreblockeddays")}
|
||||
name={["scoreboard_target", "ignoreblockeddays"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.prodtargethrs")}
|
||||
name={["prodtargethrs"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={1} precision={1} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</FeatureWrapper>
|
||||
<LayoutFormRow header={t("bodyshop.labels.systemsettings")} id="systemsettings">
|
||||
<Form.Item
|
||||
name={["md_referral_sources"]}
|
||||
@@ -567,27 +572,32 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["tt_allow_post_to_invoiced"]}
|
||||
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["tt_enforce_hours_for_tech_console"]}
|
||||
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["bill_allow_post_to_closed"]}
|
||||
label={t("bodyshop.fields.bill_allow_post_to_closed")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
{HasFeatureAccess({ featureName: "timetickets", bodyshop }) && (
|
||||
<>
|
||||
<Form.Item
|
||||
name={["tt_allow_post_to_invoiced"]}
|
||||
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["tt_enforce_hours_for_tech_console"]}
|
||||
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["bill_allow_post_to_closed"]}
|
||||
label={t("bodyshop.fields.bill_allow_post_to_closed")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item
|
||||
name={["md_ded_notes"]}
|
||||
label={t("bodyshop.fields.md_ded_notes")}
|
||||
@@ -1042,111 +1052,118 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
}}
|
||||
</Form.List>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow header={t("bodyshop.fields.md_ccc_rates")} id="md_ccc_rates">
|
||||
<Form.List name={["md_ccc_rates"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("contracts.fields.actax")} key={`${index}actax`} name={[field.name, "actax"]}>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.dailyfreekm")}
|
||||
key={`${index}dailyfreekm`}
|
||||
name={[field.name, "dailyfreekm"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.refuelcharge")}
|
||||
key={`${index}refuelcharge`}
|
||||
name={[field.name, "refuelcharge"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.excesskmrate")}
|
||||
key={`${index}excesskmrate`}
|
||||
name={[field.name, "excesskmrate"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cleanupcharge")}
|
||||
key={`${index}cleanupcharge`}
|
||||
name={[field.name, "cleanupcharge"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.damagewaiver")}
|
||||
key={`${index}damagewaiver`}
|
||||
name={[field.name, "damagewaiver"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.federaltax")}
|
||||
key={`${index}federaltax`}
|
||||
name={[field.name, "federaltax"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.statetax")}
|
||||
key={`${index}statetax`}
|
||||
name={[field.name, "statetax"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.localtax")}
|
||||
key={`${index}localtax`}
|
||||
name={[field.name, "localtax"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.coverage")}
|
||||
key={`${index}coverage`}
|
||||
name={[field.name, "coverage"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<FeatureWrapper featureName="courtesycars" noauth={() => null}>
|
||||
<LayoutFormRow grow header={t("bodyshop.fields.md_ccc_rates")} id="md_ccc_rates">
|
||||
<Form.List name={["md_ccc_rates"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.actax")}
|
||||
key={`${index}actax`}
|
||||
name={[field.name, "actax"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.dailyfreekm")}
|
||||
key={`${index}dailyfreekm`}
|
||||
name={[field.name, "dailyfreekm"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.refuelcharge")}
|
||||
key={`${index}refuelcharge`}
|
||||
name={[field.name, "refuelcharge"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.excesskmrate")}
|
||||
key={`${index}excesskmrate`}
|
||||
name={[field.name, "excesskmrate"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.cleanupcharge")}
|
||||
key={`${index}cleanupcharge`}
|
||||
name={[field.name, "cleanupcharge"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.damagewaiver")}
|
||||
key={`${index}damagewaiver`}
|
||||
name={[field.name, "damagewaiver"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.federaltax")}
|
||||
key={`${index}federaltax`}
|
||||
name={[field.name, "federaltax"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.statetax")}
|
||||
key={`${index}statetax`}
|
||||
name={[field.name, "statetax"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.localtax")}
|
||||
key={`${index}localtax`}
|
||||
name={[field.name, "localtax"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("contracts.fields.coverage")}
|
||||
key={`${index}coverage`}
|
||||
name={[field.name, "coverage"]}
|
||||
>
|
||||
<InputNumber precision={2} />
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
||||
</Space>
|
||||
</LayoutFormRow>
|
||||
<Space wrap>
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
<FormListMoveArrows move={move} index={index} total={fields.length} />
|
||||
</Space>
|
||||
</LayoutFormRow>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{t("general.actions.add")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{t("general.actions.add")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</LayoutFormRow>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</LayoutFormRow>
|
||||
</FeatureWrapper>
|
||||
|
||||
<LayoutFormRow grow header={t("bodyshop.fields.md_jobline_presets")} id="md_jobline_presets">
|
||||
<Form.List name={["md_jobline_presets"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Form, InputNumber } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -29,210 +30,219 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
return (
|
||||
<RbacWrapper action="shop:rbac">
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.exportlog")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:exportlog"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.payables")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:payables"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.payments")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:payments"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.receivables")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:receivables"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.delete")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:delete"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.enter")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:enter"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.reexport")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:reexport"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.contracts.create")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "contracts:create"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.contracts.detail")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "contracts:detail"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.contracts.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "contracts:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.courtesycar.create")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "courtesycar:create"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.courtesycar.detail")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "courtesycar:detail"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.courtesycar.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "courtesycar:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.csi.export")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "csi:export"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.csi.page")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "csi:page"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
{...HasFeatureAccess({ featureName: "export", bodyshop }) ? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.exportlog")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:exportlog"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.payables")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:payables"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.payments")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:payments"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.accounting.receivables")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "accounting:receivables"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]:[]}
|
||||
{...HasFeatureAccess({ featureName: "bills", bodyshop }) ? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.delete")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:delete"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.enter")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:enter"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.reexport")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:reexport"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]:[]}
|
||||
|
||||
{...HasFeatureAccess({ featureName: "courtesycars", bodyshop }) ? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.contracts.create")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "contracts:create"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.contracts.detail")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "contracts:detail"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.contracts.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "contracts:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.courtesycar.create")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "courtesycar:create"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.courtesycar.detail")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "courtesycar:detail"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.courtesycar.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "courtesycar:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]:[]}
|
||||
{...HasFeatureAccess({ featureName: "csi", bodyshop }) ? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.csi.export")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "csi:export"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.csi.page")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "csi:page"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]:[]}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.employees.page")}
|
||||
rules={[
|
||||
@@ -425,18 +435,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.bills.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "bills:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.employees.page")}
|
||||
rules={[
|
||||
@@ -510,18 +508,21 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.production.board")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "production:board"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
{HasFeatureAccess({ featureName: "visualboard", bodyshop }) && (
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.production.board")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "production:board"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.production.list")}
|
||||
rules={[
|
||||
@@ -546,102 +547,142 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.scoreboard.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "scoreboard:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shiftclock.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "shiftclock:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shop.config")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "shop:config"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.edit")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:edit"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:shiftedit"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.editcommitted")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:editcommitted"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.ttapprovals.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "ttapprovals:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.ttapprovals.approve")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "ttapprovals:approve"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
{HasFeatureAccess({ featureName: "scoreboard", bodyshop }) && (
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.scoreboard.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "scoreboard:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
)}
|
||||
{...HasFeatureAccess({ featureName: "timetickets", bodyshop }) ? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shiftclock.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "shiftclock:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shop.config")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "shop:config"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.edit")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:edit"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:shiftedit"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.editcommitted")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:editcommitted"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.ttapprovals.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "ttapprovals:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.ttapprovals.approve")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "ttapprovals:approve"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.enter")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:enter"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:shiftedit"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
]:[]}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shop.vendors")}
|
||||
rules={[
|
||||
@@ -690,7 +731,7 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{/* <Form.Item
|
||||
label={t("bodyshop.fields.rbac.shop.templates")}
|
||||
rules={[
|
||||
{
|
||||
@@ -701,79 +742,22 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
|
||||
name={["md_rbac", "shop:templates"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.shop.vendors")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "shop:vendors"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.temporarydocs.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "temporarydocs:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.edit")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:edit"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.enter")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:enter"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "timetickets:shiftedit"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
</Form.Item> */}
|
||||
{HasFeatureAccess({ featureName: "media", bodyshop }) && (
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.temporarydocs.view")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["md_rbac", "temporarydocs:view"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.users.editaccess")}
|
||||
rules={[
|
||||
|
||||
@@ -339,7 +339,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<div>
|
||||
<LayoutFormRow>
|
||||
<Space size='large' wrap>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<Space direction="vertical">
|
||||
@@ -386,7 +386,7 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
|
||||
</Space>
|
||||
</Form.Item>
|
||||
))}
|
||||
</LayoutFormRow>
|
||||
</Space>
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Result } from "antd";
|
||||
import { Result } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -16,19 +16,5 @@ export default connect(mapStateToProps, mapDispatchToProps)(ShopSubStatus);
|
||||
export function ShopSubStatus({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const { sub_status } = bodyshop;
|
||||
return (
|
||||
<Result
|
||||
status="403"
|
||||
title={t(`general.labels.sub_status.${sub_status}`)}
|
||||
extra={
|
||||
<Button
|
||||
onClick={() => {
|
||||
alert("Not implemented yet.");
|
||||
}}
|
||||
>
|
||||
{t("general.actions.submitticket")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
return <Result status="403" title={t(`general.labels.sub_status.${sub_status}`)} />;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -6,7 +6,8 @@ import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import VehicleDetailUpdateJobsComponent from "../vehicle-detail-update-jobs/vehicle-detail-update-jobs.component";
|
||||
|
||||
@@ -67,9 +68,20 @@ export function VehicleDetailJobsComponent({ vehicle, bodyshop }) {
|
||||
text: status,
|
||||
value: status
|
||||
})),
|
||||
onFilter: (value, record) => value.includes(record.status)
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.actual_completion"),
|
||||
dataIndex: "actual_completion",
|
||||
key: "actual_completion",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.actual_completion}</DateTimeFormatter>
|
||||
),
|
||||
sorter: (a, b) => dateSort(a.actual_completion, b.actual_completion),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "actual_completion" &&
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.clm_total"),
|
||||
dataIndex: "clm_total",
|
||||
|
||||
@@ -66,6 +66,7 @@ export const QUERY_BILLS_BY_JOBID = gql`
|
||||
order_date
|
||||
deliver_by
|
||||
return
|
||||
returnfrombill
|
||||
orderedby
|
||||
parts_order_lines {
|
||||
id
|
||||
|
||||
@@ -67,6 +67,7 @@ export const QUERY_OWNER_BY_ID = gql`
|
||||
tax_number
|
||||
jobs(order_by: { date_open: desc }) {
|
||||
id
|
||||
actual_completion
|
||||
ro_number
|
||||
clm_no
|
||||
status
|
||||
|
||||
@@ -30,6 +30,7 @@ export const QUERY_VEHICLE_BY_ID = gql`
|
||||
notes
|
||||
jobs(order_by: { date_open: desc }) {
|
||||
id
|
||||
actual_completion
|
||||
ro_number
|
||||
ownr_co_nm
|
||||
ownr_fn
|
||||
|
||||
@@ -45,7 +45,7 @@ if (import.meta.env.PROD) {
|
||||
maskAllText: false,
|
||||
blockAllMedia: true
|
||||
}),
|
||||
new Sentry.BrowserTracing({})
|
||||
new Sentry.browserTracingIntegration()
|
||||
],
|
||||
tracePropagationTargets: [
|
||||
"api.imex.online",
|
||||
|
||||
@@ -51,6 +51,17 @@ export function JobsAvailablePageContainer({ partnerVersion, setBreadcrumbs, set
|
||||
{!partnerVersion && (
|
||||
<AlertComponent
|
||||
type="warning"
|
||||
action={
|
||||
<a
|
||||
href={InstanceRenderManager({
|
||||
imex: "https://partner.imex.online/Setup.exe",
|
||||
rome: "https://partner.romeonline.io/Setup.exe",
|
||||
promanager: "https://dzaenazwrgg60.cloudfront.net/Setup.exe"
|
||||
})}
|
||||
>
|
||||
<Button size="small">{t("general.actions.download")}</Button>
|
||||
</a>
|
||||
}
|
||||
message={t("general.messages.partnernotrunning", {
|
||||
app: InstanceRenderManager({
|
||||
imex: "$t(titles.imexonline)",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -291,6 +291,7 @@ export function JobsDetailPage({
|
||||
{
|
||||
key: "general",
|
||||
icon: <Icon component={FaShieldAlt} />,
|
||||
id: "job-details-general",
|
||||
label: t("menus.jobsdetail.general"),
|
||||
forceRender: true,
|
||||
children: <JobsDetailGeneral job={job} form={form} />
|
||||
@@ -298,6 +299,7 @@ export function JobsDetailPage({
|
||||
{
|
||||
key: "repairdata",
|
||||
icon: <BarsOutlined />,
|
||||
id: "job-details-repairdata",
|
||||
label: t("menus.jobsdetail.repairdata"),
|
||||
forceRender: true,
|
||||
children: <JobsLinesContainer job={job} joblines={job.joblines} refetch={refetch} form={form} />
|
||||
@@ -305,18 +307,21 @@ export function JobsDetailPage({
|
||||
{
|
||||
key: "rates",
|
||||
icon: <DollarCircleOutlined />,
|
||||
id: "job-details-rates",
|
||||
label: t("menus.jobsdetail.rates"),
|
||||
forceRender: true,
|
||||
children: <JobsDetailRates job={job} form={form} />
|
||||
},
|
||||
{
|
||||
key: "totals",
|
||||
id: "job-details-totals",
|
||||
icon: <DollarCircleOutlined />,
|
||||
label: t("menus.jobsdetail.totals"),
|
||||
children: <JobsDetailTotals job={job} refetch={refetch} />
|
||||
},
|
||||
{
|
||||
key: "partssublet",
|
||||
id: "job-details-partssublet",
|
||||
icon: <ToolFilled />,
|
||||
label: HasFeatureAccess({ featureName: "bills", bodyshop })
|
||||
? t("menus.jobsdetail.partssublet")
|
||||
@@ -331,6 +336,7 @@ export function JobsDetailPage({
|
||||
? [
|
||||
{
|
||||
key: "labor",
|
||||
id: "job-details-labor",
|
||||
icon: <Icon component={FaHardHat} />,
|
||||
label: t("menus.jobsdetail.labor"),
|
||||
children: <JobsDetailLaborContainer job={job} jobId={job.id} />
|
||||
@@ -340,11 +346,13 @@ export function JobsDetailPage({
|
||||
{
|
||||
key: "lifecycle",
|
||||
icon: <BarsOutlined />,
|
||||
id: "job-details-lifecycle",
|
||||
label: t("menus.jobsdetail.lifecycle"),
|
||||
children: <JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses} />
|
||||
},
|
||||
{
|
||||
key: "dates",
|
||||
id: "job-details-dates",
|
||||
icon: <CalendarFilled />,
|
||||
label: t("menus.jobsdetail.dates"),
|
||||
forceRender: true,
|
||||
@@ -358,6 +366,7 @@ export function JobsDetailPage({
|
||||
? [
|
||||
{
|
||||
key: "documents",
|
||||
id: "job-details-documents",
|
||||
icon: <FileImageFilled />,
|
||||
label: t("jobs.labels.documents"),
|
||||
children: bodyshop.uselocalmediaserver ? (
|
||||
@@ -370,6 +379,7 @@ export function JobsDetailPage({
|
||||
: []),
|
||||
{
|
||||
key: "notes",
|
||||
id: "job-details-notes",
|
||||
icon: <Icon component={FaRegStickyNote} />,
|
||||
label: t("jobs.labels.notes"),
|
||||
children: <JobNotesContainer jobId={job.id} />
|
||||
@@ -377,12 +387,14 @@ export function JobsDetailPage({
|
||||
{
|
||||
key: "audit",
|
||||
icon: <HistoryOutlined />,
|
||||
id: "job-details-audit",
|
||||
label: t("jobs.labels.audit"),
|
||||
children: <JobAuditTrail jobId={job.id} />
|
||||
},
|
||||
{
|
||||
key: "tasks",
|
||||
icon: <FaTasks />,
|
||||
id: "job-details-tasks",
|
||||
label: (
|
||||
<Space direction="horizontal">
|
||||
{t("jobs.labels.tasks")}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Collapse, FloatButton, Layout, Space, Spin, Tag } from "antd";
|
||||
import { FloatButton, Layout, Spin } from "antd";
|
||||
// import preval from "preval.macro";
|
||||
import React, { lazy, Suspense, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -117,7 +117,6 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export function Manage({ conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyRideFinished }) {
|
||||
const { t } = useTranslation();
|
||||
const [chatVisible] = useState(false);
|
||||
const [tours, setTours] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const widgetId = InstanceRenderManager({
|
||||
@@ -630,28 +629,6 @@ export function Manage({ conflict, bodyshop, enableJoyRide, joyRideSteps, setJoy
|
||||
Disclaimer & Notices
|
||||
</Link>
|
||||
</div>
|
||||
{InstanceRenderManager({
|
||||
promanager: (
|
||||
<Collapse>
|
||||
<Collapse.Panel header="DEVELOPMENT ONLY - ProductFruits Tours">
|
||||
<Space>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setTours(await window.productFruits.api.tours.getTours());
|
||||
}}
|
||||
>
|
||||
Get Tours
|
||||
</Button>
|
||||
{tours.map((tour) => (
|
||||
<Tag key={tour.id} onClick={() => window.productFruits.api.tours.tryStartTour(tour.id)}>
|
||||
{tour.name}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
)
|
||||
})}
|
||||
</Footer>
|
||||
</Layout>
|
||||
</>
|
||||
|
||||
@@ -72,10 +72,17 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
key: "licensing",
|
||||
label: t("bodyshop.labels.licensing"),
|
||||
children: <ShopInfoUsersComponent />
|
||||
InstanceRenderManager({
|
||||
executeFunction: true,
|
||||
args: [],
|
||||
imex: () => {
|
||||
items.push({
|
||||
key: "licensing",
|
||||
label: t("bodyshop.labels.licensing"),
|
||||
children: <ShopInfoUsersComponent />
|
||||
});
|
||||
},
|
||||
rome: "USE_IMEX"
|
||||
});
|
||||
|
||||
if (HasFeatureAccess({ featureName: "csi", bodyshop })) {
|
||||
|
||||
@@ -8,7 +8,8 @@ const INITIAL_STATE = {
|
||||
name: "ShopName",
|
||||
address: InstanceRenderManager({
|
||||
imex: "noreply@iemx.online",
|
||||
rome: "noreply@romeonline.io"
|
||||
rome: "noreply@romeonline.io",
|
||||
promanager: "noreply@promanager.web-est.com"
|
||||
})
|
||||
},
|
||||
to: null,
|
||||
|
||||
@@ -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;
|
||||
@@ -15,6 +15,7 @@ import { getToken } from "firebase/messaging";
|
||||
import i18next from "i18next";
|
||||
import LogRocket from "logrocket";
|
||||
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
||||
import { Userpilot } from "userpilot";
|
||||
import { factory } from "../../App/App.container";
|
||||
import {
|
||||
analytics,
|
||||
@@ -25,6 +26,10 @@ import {
|
||||
messaging,
|
||||
updateCurrentUser
|
||||
} from "../../firebase/firebase.utils";
|
||||
import { QUERY_EULA } from "../../graphql/bodyshop.queries";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
import day from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import {
|
||||
checkInstanceId,
|
||||
sendPasswordResetFailure,
|
||||
@@ -43,11 +48,6 @@ import {
|
||||
validatePasswordResetSuccess
|
||||
} from "./user.actions";
|
||||
import UserActionTypes from "./user.types";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
import { QUERY_EULA } from "../../graphql/bodyshop.queries";
|
||||
import day from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { Userpilot } from "userpilot";
|
||||
|
||||
const fpPromise = FingerprintJS.load();
|
||||
|
||||
@@ -310,10 +310,41 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
updateUserDetailsSuccess(authRecord[0] ? { validemail: authRecord[0].user.validemail } : { validemail: false })
|
||||
);
|
||||
|
||||
const user = yield select((state) => state.user.currentUser);
|
||||
if (payload.features.singleDeviceOnly) {
|
||||
const user = yield select((state) => state.user.currentUser);
|
||||
if (
|
||||
!(
|
||||
user.email.includes("@imex.") ||
|
||||
user.email.includes("@rome.") ||
|
||||
user.email.includes("@rometech.") ||
|
||||
user.email.includes("@promanager.")
|
||||
)
|
||||
)
|
||||
yield put(setInstanceId(user.uid));
|
||||
}
|
||||
|
||||
if (!(user.email.includes("@imex.") || user.email.includes("@rome."))) yield put(setInstanceId(user.uid));
|
||||
//For Rome, check to make sure it's not a PM shop.
|
||||
try {
|
||||
InstanceRenderManager({
|
||||
executeFunction: true,
|
||||
args: [],
|
||||
rome: () => {
|
||||
if (
|
||||
payload.imexshopid.toLowerCase().startsWith("pm_") &&
|
||||
!(
|
||||
user.email.includes("@imex.") ||
|
||||
user.email.includes("@rome.") ||
|
||||
user.email.includes("@rometech.") ||
|
||||
user.email.includes("@promanager.")
|
||||
)
|
||||
) {
|
||||
throw new Error("You are not authorized to use this application.");
|
||||
}
|
||||
},
|
||||
promanager: () => {}
|
||||
});
|
||||
} catch (error) {
|
||||
yield put(setInstanceConflict());
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user