Compare commits
82 Commits
master-AIO
...
test-AIO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
045f36e294 | ||
|
|
c7c6dfcd7d | ||
|
|
c024fdd57b | ||
|
|
a4ccacf83a | ||
|
|
fdaf50d778 | ||
|
|
b887cfed01 | ||
|
|
6cce92b0fd | ||
|
|
60ab04cb38 | ||
|
|
0025e113c6 | ||
|
|
dc435b2bb0 | ||
|
|
fd72d244e7 | ||
|
|
87bb472271 | ||
|
|
825959880e | ||
|
|
c40fea0ec9 | ||
|
|
ebdf427b58 | ||
|
|
b3fdd68276 | ||
|
|
30e5027c8c | ||
|
|
3e63c58b9b | ||
|
|
938cef1f6b | ||
|
|
7e2df3e341 | ||
|
|
45d095a7a3 | ||
|
|
709b6ef1d6 | ||
|
|
4e98df6694 | ||
|
|
b920bb4437 | ||
|
|
e36a110e81 | ||
|
|
719d1b6479 | ||
|
|
29ded5efbf | ||
|
|
551e0f0592 | ||
|
|
4d299bb226 | ||
|
|
ae9b68a0bc | ||
|
|
cf8df89e30 | ||
|
|
bfd6cc83af | ||
|
|
99b65e8186 | ||
|
|
f8fd2ee64c | ||
|
|
8240ea9a64 | ||
|
|
85b3b88538 | ||
|
|
426283ffee | ||
|
|
4fc86ccaa3 | ||
|
|
519997a8be | ||
|
|
c4c36b7fd0 | ||
|
|
deb2fc28ce | ||
|
|
a67946c5a3 | ||
|
|
e43923b7a0 | ||
|
|
e9ef429729 | ||
|
|
db01ad9155 | ||
|
|
8bf7fbd1f1 | ||
|
|
c37037ef21 | ||
|
|
6050aebcd5 | ||
|
|
77d0f5ab38 | ||
|
|
a0692f8c69 | ||
|
|
4f76aeb06f | ||
|
|
302a42089f | ||
|
|
906265c4b2 | ||
|
|
388b042037 | ||
|
|
73eb76a230 | ||
|
|
d5e9b79f75 | ||
|
|
56d0c009e2 | ||
|
|
79030f6b36 | ||
|
|
5e78cdd8ae | ||
|
|
8f4ac866f1 | ||
|
|
9ad2a53bec | ||
|
|
6590f8961b | ||
|
|
7df71b8f44 | ||
|
|
4776b03a21 | ||
|
|
20943f74e9 | ||
|
|
4af312854e | ||
|
|
ff084f6fb8 | ||
|
|
5c9e4517a6 | ||
|
|
190217ffce | ||
|
|
28dc1d4533 | ||
|
|
a97e03e0b1 | ||
|
|
e30353cab6 | ||
|
|
c9b9f67170 | ||
|
|
4a47f543b2 | ||
|
|
3b60aa89f1 | ||
|
|
20d2572087 | ||
|
|
ac4c09af60 | ||
|
|
6a60af9dfe | ||
|
|
dfb6f02864 | ||
|
|
48bb494e0f | ||
|
|
9b74cba56b | ||
|
|
6fc8124268 |
@@ -7,6 +7,7 @@ _reference
|
|||||||
client
|
client
|
||||||
redis/dockerdata
|
redis/dockerdata
|
||||||
hasura
|
hasura
|
||||||
|
harness-feature-flags-export
|
||||||
node_modules
|
node_modules
|
||||||
# Files to exclude
|
# Files to exclude
|
||||||
.ebignore
|
.ebignore
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
/client
|
/client
|
||||||
/firebase
|
/firebase
|
||||||
/hasura
|
/hasura
|
||||||
|
/harness-feature-flags-export
|
||||||
/jsreport
|
/jsreport
|
||||||
/node_modules
|
/node_modules
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -17,6 +17,9 @@ jsreport/auth-server/node_modules
|
|||||||
client/coverage
|
client/coverage
|
||||||
admin/coverage
|
admin/coverage
|
||||||
|
|
||||||
|
# Generated Harness/Split feature flag export artifacts
|
||||||
|
/harness-feature-flags-export/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
client/build
|
client/build
|
||||||
@@ -153,4 +156,5 @@ docker_data
|
|||||||
|
|
||||||
.terraform
|
.terraform
|
||||||
|
|
||||||
terraform.tfvars
|
terraform.tfvars
|
||||||
|
terraform.exe
|
||||||
|
|||||||
1297
_reference/feature-flags.md
Normal file
1297
_reference/feature-flags.md
Normal file
File diff suppressed because it is too large
Load Diff
3193
client/package-lock.json
generated
3193
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,62 +8,61 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@amplitude/analytics-browser": "^2.38.0",
|
"@amplitude/analytics-browser": "^2.42.4",
|
||||||
"@ant-design/pro-layout": "^7.22.6",
|
"@ant-design/pro-layout": "^7.22.6",
|
||||||
"@apollo/client": "^4.1.6",
|
"@apollo/client": "^4.2.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@documenso/embed-react": "^0.5.1",
|
"@documenso/embed-react": "^0.6.1",
|
||||||
"@emotion/is-prop-valid": "^1.4.0",
|
"@emotion/is-prop-valid": "^1.4.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^5.1.0",
|
"@fingerprintjs/fingerprintjs": "^5.2.0",
|
||||||
"@firebase/analytics": "^0.10.21",
|
"@firebase/analytics": "^0.10.22",
|
||||||
"@firebase/app": "^0.14.10",
|
"@firebase/app": "^0.14.12",
|
||||||
"@firebase/auth": "^1.12.2",
|
"@firebase/auth": "^1.13.1",
|
||||||
"@firebase/firestore": "^4.13.0",
|
"@firebase/firestore": "^4.14.1",
|
||||||
"@firebase/messaging": "^0.12.25",
|
"@firebase/messaging": "^0.12.26",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.11.2",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
"@sentry/cli": "^3.3.5",
|
"@sentry/cli": "^3.4.3",
|
||||||
"@sentry/react": "^10.47.0",
|
"@sentry/react": "^10.53.1",
|
||||||
"@sentry/vite-plugin": "^4.9.1",
|
"@sentry/vite-plugin": "^4.9.1",
|
||||||
"@splitsoftware/splitio-react": "^2.6.1",
|
|
||||||
"@tanem/react-nprogress": "^5.0.63",
|
"@tanem/react-nprogress": "^5.0.63",
|
||||||
"antd": "^6.3.5",
|
"antd": "^6.4.3",
|
||||||
"apollo-link-logger": "^3.0.0",
|
"apollo-link-logger": "^3.0.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.14.0",
|
"axios": "^1.16.1",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"css-box-model": "^1.2.1",
|
"css-box-model": "^1.2.1",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"dayjs-business-days2": "^1.3.3",
|
"dayjs-business-days2": "^1.3.3",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.4.2",
|
||||||
"env-cmd": "^11.0.0",
|
"env-cmd": "^11.0.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"graphql": "^16.13.2",
|
"graphql": "^16.14.0",
|
||||||
"graphql-ws": "^6.0.8",
|
"graphql-ws": "^6.0.8",
|
||||||
"i18next": "^25.10.10",
|
"i18next": "^25.10.10",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"libphonenumber-js": "^1.12.41",
|
"libphonenumber-js": "^1.13.3",
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"logrocket": "^12.1.0",
|
"logrocket": "^12.1.1",
|
||||||
"markerjs2": "^2.32.7",
|
"markerjs2": "^2.32.7",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.1.1",
|
"normalize-url": "^8.1.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"phone": "^3.1.71",
|
"phone": "^3.1.71",
|
||||||
"posthog-js": "^1.364.4",
|
"posthog-js": "^1.376.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.3.1",
|
"query-string": "^9.3.1",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.6",
|
||||||
"react-big-calendar": "^1.19.4",
|
"react-big-calendar": "^1.19.4",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^8.1.0",
|
"react-cookie": "^8.1.2",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.6",
|
||||||
"react-grid-gallery": "^1.0.1",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "^2.2.3",
|
"react-grid-layout": "^2.2.3",
|
||||||
"react-i18next": "^16.6.6",
|
"react-i18next": "^16.6.6",
|
||||||
@@ -73,22 +72,22 @@
|
|||||||
"react-number-format": "^5.4.5",
|
"react-number-format": "^5.4.5",
|
||||||
"react-popopo": "^2.1.9",
|
"react-popopo": "^2.1.9",
|
||||||
"react-product-fruits": "^2.2.62",
|
"react-product-fruits": "^2.2.62",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.3.0",
|
||||||
"react-resizable": "^3.1.3",
|
"react-resizable": "^3.1.3",
|
||||||
"react-router-dom": "^7.13.2",
|
"react-router-dom": "^7.15.1",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtuoso": "^4.18.3",
|
"react-virtuoso": "^4.18.7",
|
||||||
"recharts": "^3.8.1",
|
"recharts": "^3.8.1",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-actions": "^3.0.3",
|
"redux-actions": "^3.0.3",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.4.2",
|
"redux-saga": "^1.5.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.2.0",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"sass": "^1.98.0",
|
"sass": "^1.100.0",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"styled-components": "^6.3.12",
|
"styled-components": "^6.4.2",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
"web-vitals": "^5.2.0"
|
"web-vitals": "^5.2.0"
|
||||||
},
|
},
|
||||||
@@ -138,14 +137,14 @@
|
|||||||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/icons": "^6.1.1",
|
"@ant-design/icons": "^6.2.3",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.28.5",
|
"@babel/preset-react": "^7.29.7",
|
||||||
"@dotenvx/dotenvx": "^1.59.1",
|
"@dotenvx/dotenvx": "^1.68.1",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.60.0",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
@@ -157,21 +156,21 @@
|
|||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.6.0",
|
||||||
"jsdom": "^28.1.0",
|
"jsdom": "^28.1.0",
|
||||||
"memfs": "^4.57.1",
|
"memfs": "^4.57.2",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"playwright": "^1.58.2",
|
"playwright": "^1.60.0",
|
||||||
"react-error-overlay": "^6.1.0",
|
"react-error-overlay": "^6.1.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-babel": "^1.6.0",
|
"vite-plugin-babel": "^1.7.3",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-node-polyfills": "^0.26.0",
|
"vite-plugin-node-polyfills": "^0.28.0",
|
||||||
"vite-plugin-pwa": "^1.2.0",
|
"vite-plugin-pwa": "^1.3.0",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"vitest": "^4.1.2",
|
"vitest": "^4.1.7",
|
||||||
"workbox-window": "^7.4.0"
|
"workbox-window": "^7.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5593,29 +5593,6 @@ Demo: https://rawgit.com/Sphinxxxx/color-conversion/master/demo/index.html
|
|||||||
|
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
The following NPM packages may be included in this product:
|
|
||||||
|
|
||||||
- @splitsoftware/splitio-commons@1.6.1
|
|
||||||
- @splitsoftware/splitio-react@1.7.1
|
|
||||||
|
|
||||||
These packages each contain the following license and notice below:
|
|
||||||
|
|
||||||
Copyright © 2022 Split Software, Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
|
|
||||||
-----------
|
|
||||||
|
|
||||||
The following NPM packages may be included in this product:
|
The following NPM packages may be included in this product:
|
||||||
|
|
||||||
- @stripe/react-stripe-js@1.9.0
|
- @stripe/react-stripe-js@1.9.0
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
import { ApolloProvider } from "@apollo/client/react";
|
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
|
||||||
import { ConfigProvider, Grid } from "antd";
|
|
||||||
import enLocale from "antd/es/locale/en_US";
|
|
||||||
import { useEffect, useMemo } from "react";
|
|
||||||
import { CookiesProvider } from "react-cookie";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
|
||||||
import { setDarkMode } from "../redux/application/application.actions";
|
|
||||||
import { selectDarkMode } from "../redux/application/application.selectors";
|
|
||||||
import { selectCurrentUser } from "../redux/user/user.selectors.js";
|
|
||||||
import { signOutStart } from "../redux/user/user.actions";
|
|
||||||
import client from "../utils/GraphQLClient";
|
|
||||||
import App from "./App";
|
|
||||||
import getTheme from "./themeProvider";
|
|
||||||
|
|
||||||
// Base Split configuration
|
|
||||||
const config = {
|
|
||||||
core: {
|
|
||||||
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
|
||||||
key: "anon"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function SplitClientProvider({ children }) {
|
|
||||||
const imexshopid = useSelector((state) => state.user.imexshopid);
|
|
||||||
const splitClient = useSplitClient({ key: imexshopid || "anon" });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (import.meta.env.DEV && splitClient && imexshopid) {
|
|
||||||
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
|
||||||
}
|
|
||||||
}, [splitClient, imexshopid]);
|
|
||||||
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AppContainer() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const currentUser = useSelector(selectCurrentUser);
|
|
||||||
const isDarkMode = useSelector(selectDarkMode);
|
|
||||||
const screens = Grid.useBreakpoint();
|
|
||||||
const isPhone = !screens.md;
|
|
||||||
const isUltraWide = Boolean(screens.xxxl);
|
|
||||||
|
|
||||||
const theme = useMemo(() => {
|
|
||||||
const baseTheme = getTheme(isDarkMode);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...baseTheme,
|
|
||||||
token: {
|
|
||||||
...(baseTheme.token || {}),
|
|
||||||
screenXXXL: 2160
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
...(baseTheme.components || {}),
|
|
||||||
Table: {
|
|
||||||
...(baseTheme.components?.Table || {}),
|
|
||||||
cellFontSizeSM: isPhone ? 12 : 13,
|
|
||||||
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
|
|
||||||
cellFontSize: isUltraWide ? 15 : 14,
|
|
||||||
cellPaddingInlineSM: isPhone ? 8 : 10,
|
|
||||||
cellPaddingInlineMD: isPhone ? 10 : 14,
|
|
||||||
cellPaddingInline: isUltraWide ? 20 : 16,
|
|
||||||
cellPaddingBlockSM: isPhone ? 8 : 10,
|
|
||||||
cellPaddingBlockMD: isPhone ? 10 : 12,
|
|
||||||
cellPaddingBlock: isUltraWide ? 14 : 12,
|
|
||||||
selectionColumnWidth: isPhone ? 44 : 52
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [isDarkMode, isPhone, isUltraWide]);
|
|
||||||
|
|
||||||
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
|
|
||||||
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
|
|
||||||
const antdPagination = useMemo(
|
|
||||||
() => ({
|
|
||||||
showSizeChanger: !isPhone,
|
|
||||||
totalBoundaryShowSizeChanger: 100
|
|
||||||
}),
|
|
||||||
[isPhone]
|
|
||||||
);
|
|
||||||
|
|
||||||
const antdForm = useMemo(
|
|
||||||
() => ({
|
|
||||||
validateMessages: {
|
|
||||||
required: t("general.validation.required", { label: "${label}" })
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[t]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Global seamless logout listener with redirect to /signin
|
|
||||||
useEffect(() => {
|
|
||||||
const handleSeamlessLogout = (event) => {
|
|
||||||
if (event.data?.type !== "seamlessLogoutRequest") return;
|
|
||||||
|
|
||||||
// Only accept messages from the parent window
|
|
||||||
if (event.source !== window.parent) return;
|
|
||||||
|
|
||||||
const targetOrigin = event.origin || "*";
|
|
||||||
|
|
||||||
if (currentUser?.authorized !== true) {
|
|
||||||
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(signOutStart());
|
|
||||||
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("message", handleSeamlessLogout);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("message", handleSeamlessLogout);
|
|
||||||
};
|
|
||||||
}, [dispatch, currentUser?.authorized]);
|
|
||||||
|
|
||||||
// Update data-theme attribute (no cleanup to avoid transient style churn)
|
|
||||||
useEffect(() => {
|
|
||||||
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
|
|
||||||
}, [isDarkMode]);
|
|
||||||
|
|
||||||
// Sync darkMode with localStorage
|
|
||||||
useEffect(() => {
|
|
||||||
const uid = currentUser?.uid;
|
|
||||||
|
|
||||||
if (!uid) {
|
|
||||||
dispatch(setDarkMode(false));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = `dark-mode-${uid}`;
|
|
||||||
const raw = localStorage.getItem(key);
|
|
||||||
|
|
||||||
if (raw == null) {
|
|
||||||
dispatch(setDarkMode(false));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
|
|
||||||
} catch {
|
|
||||||
dispatch(setDarkMode(false));
|
|
||||||
}
|
|
||||||
}, [currentUser?.uid, dispatch]);
|
|
||||||
|
|
||||||
// Persist darkMode
|
|
||||||
useEffect(() => {
|
|
||||||
const uid = currentUser?.uid;
|
|
||||||
if (!uid) return;
|
|
||||||
|
|
||||||
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
|
|
||||||
}, [isDarkMode, currentUser?.uid]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CookiesProvider>
|
|
||||||
<ApolloProvider client={client}>
|
|
||||||
<ConfigProvider
|
|
||||||
input={antdInput}
|
|
||||||
locale={enLocale}
|
|
||||||
theme={theme}
|
|
||||||
form={antdForm}
|
|
||||||
table={antdTable}
|
|
||||||
pagination={antdPagination}
|
|
||||||
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
|
|
||||||
popupOverflow="viewport"
|
|
||||||
>
|
|
||||||
<GlobalLoadingBar />
|
|
||||||
<SplitFactoryProvider config={config}>
|
|
||||||
<SplitClientProvider>
|
|
||||||
<App />
|
|
||||||
</SplitClientProvider>
|
|
||||||
</SplitFactoryProvider>
|
|
||||||
</ConfigProvider>
|
|
||||||
</ApolloProvider>
|
|
||||||
</CookiesProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Sentry.withProfiler(AppContainer);
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ApolloProvider } from "@apollo/client/react";
|
import { ApolloProvider } from "@apollo/client/react";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
import { FeatureFlagProvider, useFeatureFlagClient } from "../feature-flags/splitio-react-replacement";
|
||||||
import { ConfigProvider } from "antd";
|
import { ConfigProvider } from "antd";
|
||||||
import enLocale from "antd/es/locale/en_US";
|
import enLocale from "antd/es/locale/en_US";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
@@ -16,23 +16,21 @@ import client from "../utils/GraphQLClient";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
import getTheme from "./themeProvider";
|
import getTheme from "./themeProvider";
|
||||||
|
|
||||||
// Base Split configuration
|
|
||||||
const config = {
|
const config = {
|
||||||
core: {
|
core: {
|
||||||
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
|
||||||
key: "anon"
|
key: "anon"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function SplitClientProvider({ children }) {
|
function FeatureFlagClientProvider({ children }) {
|
||||||
const imexshopid = useSelector((state) => state.user.imexshopid);
|
const imexshopid = useSelector((state) => state.user.imexshopid);
|
||||||
const splitClient = useSplitClient({ key: imexshopid || "anon" });
|
const featureFlagClient = useFeatureFlagClient({ key: imexshopid || "anon" });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (import.meta.env.DEV && splitClient && imexshopid) {
|
if (import.meta.env.DEV && featureFlagClient && imexshopid) {
|
||||||
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
console.log(`Feature flag client initialized with key: ${imexshopid}, isReady: ${featureFlagClient.isReady}`);
|
||||||
}
|
}
|
||||||
}, [splitClient, imexshopid]);
|
}, [featureFlagClient, imexshopid]);
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
@@ -124,11 +122,11 @@ function AppContainer() {
|
|||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<ConfigProvider input={antdInput} locale={enLocale} theme={theme} form={antdForm}>
|
<ConfigProvider input={antdInput} locale={enLocale} theme={theme} form={antdForm}>
|
||||||
<GlobalLoadingBar />
|
<GlobalLoadingBar />
|
||||||
<SplitFactoryProvider config={config}>
|
<FeatureFlagProvider config={config}>
|
||||||
<SplitClientProvider>
|
<FeatureFlagClientProvider>
|
||||||
<App />
|
<App />
|
||||||
</SplitClientProvider>
|
</FeatureFlagClientProvider>
|
||||||
</SplitFactoryProvider>
|
</FeatureFlagProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
</CookiesProvider>
|
</CookiesProvider>
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
import { ApolloProvider } from "@apollo/client/react";
|
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
|
||||||
import { ConfigProvider, Grid } from "antd";
|
|
||||||
import enLocale from "antd/es/locale/en_US";
|
|
||||||
import { useEffect, useMemo } from "react";
|
|
||||||
import { CookiesProvider } from "react-cookie";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
|
||||||
import { setDarkMode } from "../redux/application/application.actions";
|
|
||||||
import { selectDarkMode } from "../redux/application/application.selectors";
|
|
||||||
import { selectCurrentUser } from "../redux/user/user.selectors.js";
|
|
||||||
import { signOutStart } from "../redux/user/user.actions";
|
|
||||||
import client from "../utils/GraphQLClient";
|
|
||||||
import App from "./App";
|
|
||||||
import getTheme from "./themeProvider";
|
|
||||||
|
|
||||||
// Base Split configuration
|
|
||||||
const config = {
|
|
||||||
core: {
|
|
||||||
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
|
||||||
key: "anon"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function SplitClientProvider({ children }) {
|
|
||||||
const imexshopid = useSelector((state) => state.user.imexshopid);
|
|
||||||
const splitClient = useSplitClient({ key: imexshopid || "anon" });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (import.meta.env.DEV && splitClient && imexshopid) {
|
|
||||||
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
|
||||||
}
|
|
||||||
}, [splitClient, imexshopid]);
|
|
||||||
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AppContainer() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const currentUser = useSelector(selectCurrentUser);
|
|
||||||
const isDarkMode = useSelector(selectDarkMode);
|
|
||||||
const screens = Grid.useBreakpoint();
|
|
||||||
const isPhone = !screens.md;
|
|
||||||
const isUltraWide = Boolean(screens.xxxl);
|
|
||||||
|
|
||||||
const theme = useMemo(() => {
|
|
||||||
const baseTheme = getTheme(isDarkMode);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...baseTheme,
|
|
||||||
token: {
|
|
||||||
...(baseTheme.token || {}),
|
|
||||||
screenXXXL: 2160
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
...(baseTheme.components || {}),
|
|
||||||
Table: {
|
|
||||||
...(baseTheme.components?.Table || {}),
|
|
||||||
cellFontSizeSM: isPhone ? 12 : 13,
|
|
||||||
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
|
|
||||||
cellFontSize: isUltraWide ? 15 : 14,
|
|
||||||
cellPaddingInlineSM: isPhone ? 8 : 10,
|
|
||||||
cellPaddingInlineMD: isPhone ? 10 : 14,
|
|
||||||
cellPaddingInline: isUltraWide ? 20 : 16,
|
|
||||||
cellPaddingBlockSM: isPhone ? 8 : 10,
|
|
||||||
cellPaddingBlockMD: isPhone ? 10 : 12,
|
|
||||||
cellPaddingBlock: isUltraWide ? 14 : 12,
|
|
||||||
selectionColumnWidth: isPhone ? 44 : 52
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [isDarkMode, isPhone, isUltraWide]);
|
|
||||||
|
|
||||||
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
|
|
||||||
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
|
|
||||||
const antdPagination = useMemo(
|
|
||||||
() => ({
|
|
||||||
showSizeChanger: !isPhone,
|
|
||||||
totalBoundaryShowSizeChanger: 100
|
|
||||||
}),
|
|
||||||
[isPhone]
|
|
||||||
);
|
|
||||||
|
|
||||||
const antdForm = useMemo(
|
|
||||||
() => ({
|
|
||||||
validateMessages: {
|
|
||||||
required: t("general.validation.required", { label: "${label}" })
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[t]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Global seamless logout listener with redirect to /signin
|
|
||||||
useEffect(() => {
|
|
||||||
const handleSeamlessLogout = (event) => {
|
|
||||||
if (event.data?.type !== "seamlessLogoutRequest") return;
|
|
||||||
|
|
||||||
// Only accept messages from the parent window
|
|
||||||
if (event.source !== window.parent) return;
|
|
||||||
|
|
||||||
const targetOrigin = event.origin || "*";
|
|
||||||
|
|
||||||
if (currentUser?.authorized !== true) {
|
|
||||||
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(signOutStart());
|
|
||||||
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("message", handleSeamlessLogout);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("message", handleSeamlessLogout);
|
|
||||||
};
|
|
||||||
}, [dispatch, currentUser?.authorized]);
|
|
||||||
|
|
||||||
// Update data-theme attribute (no cleanup to avoid transient style churn)
|
|
||||||
useEffect(() => {
|
|
||||||
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
|
|
||||||
}, [isDarkMode]);
|
|
||||||
|
|
||||||
// Sync darkMode with localStorage
|
|
||||||
useEffect(() => {
|
|
||||||
const uid = currentUser?.uid;
|
|
||||||
|
|
||||||
if (!uid) {
|
|
||||||
dispatch(setDarkMode(false));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = `dark-mode-${uid}`;
|
|
||||||
const raw = localStorage.getItem(key);
|
|
||||||
|
|
||||||
if (raw == null) {
|
|
||||||
dispatch(setDarkMode(false));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
|
|
||||||
} catch {
|
|
||||||
dispatch(setDarkMode(false));
|
|
||||||
}
|
|
||||||
}, [currentUser?.uid, dispatch]);
|
|
||||||
|
|
||||||
// Persist darkMode
|
|
||||||
useEffect(() => {
|
|
||||||
const uid = currentUser?.uid;
|
|
||||||
if (!uid) return;
|
|
||||||
|
|
||||||
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
|
|
||||||
}, [isDarkMode, currentUser?.uid]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CookiesProvider>
|
|
||||||
<ApolloProvider client={client}>
|
|
||||||
<ConfigProvider
|
|
||||||
input={antdInput}
|
|
||||||
locale={enLocale}
|
|
||||||
theme={theme}
|
|
||||||
form={antdForm}
|
|
||||||
table={antdTable}
|
|
||||||
pagination={antdPagination}
|
|
||||||
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
|
|
||||||
popupOverflow="viewport"
|
|
||||||
>
|
|
||||||
<GlobalLoadingBar />
|
|
||||||
<SplitFactoryProvider config={config}>
|
|
||||||
<SplitClientProvider>
|
|
||||||
<App />
|
|
||||||
</SplitClientProvider>
|
|
||||||
</SplitFactoryProvider>
|
|
||||||
</ConfigProvider>
|
|
||||||
</ApolloProvider>
|
|
||||||
</CookiesProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Sentry.withProfiler(AppContainer);
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useSplitClient } from "@splitsoftware/splitio-react";
|
import { useSplitClient } from "../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Result } from "antd";
|
import { Button, Result } from "antd";
|
||||||
//import LogRocket from "logrocket";
|
//import LogRocket from "logrocket";
|
||||||
import { lazy, Suspense, useEffect, useState } from "react";
|
import { lazy, Suspense, useEffect, useState } from "react";
|
||||||
@@ -225,13 +225,22 @@ export function App({
|
|||||||
path="/parts/*"
|
path="/parts/*"
|
||||||
element={
|
element={
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||||
|
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||||
|
</SocketProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Route path="*" element={<SimplifiedPartsPageContainer />} />
|
<Route path="*" element={<SimplifiedPartsPageContainer />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized} />}>
|
<Route
|
||||||
|
path="/edit/*"
|
||||||
|
element={
|
||||||
|
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||||
|
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||||
|
</SocketProvider>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Route path="*" element={<DocumentEditorContainer />} />
|
<Route path="*" element={<DocumentEditorContainer />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -512,7 +512,52 @@
|
|||||||
|
|
||||||
|
|
||||||
.esignature-embed {
|
.esignature-embed {
|
||||||
width: 100%;
|
display: block;
|
||||||
height: 100%;
|
width: 100%;
|
||||||
border-width: 0;
|
height: 100%;
|
||||||
}
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.esignature-modal {
|
||||||
|
.ant-modal {
|
||||||
|
top: 16px;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: calc(100vh - 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.esignature-modal-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 150px);
|
||||||
|
min-height: 320px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px), (max-height: 560px) {
|
||||||
|
.esignature-modal {
|
||||||
|
.ant-modal {
|
||||||
|
top: 8px;
|
||||||
|
max-width: calc(100vw - 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-content {
|
||||||
|
max-height: calc(100vh - 16px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.esignature-modal-frame {
|
||||||
|
height: calc(100vh - 132px);
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const defaultTheme = (isDarkMode) => ({
|
|||||||
isDarkMode
|
isDarkMode
|
||||||
),
|
),
|
||||||
colorError: isDarkMode ? "#ff4d4f" : "#f5222d",
|
colorError: isDarkMode ? "#ff4d4f" : "#f5222d",
|
||||||
|
colorShadow: isDarkMode ? "rgba(0, 0, 0, 0.7)" : "#000000",
|
||||||
colorBgBase: isDarkMode ? "#1f1f1f" : "#ffffff" // Align with Ant Design dark mode
|
colorBgBase: isDarkMode ? "#1f1f1f" : "#ffffff" // Align with Ant Design dark mode
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useApolloClient, useMutation } from "@apollo/client/react";
|
import { useApolloClient, useMutation } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Checkbox, Divider, Form, Modal, Space } from "antd";
|
import { Button, Checkbox, Divider, Form, Modal, Space } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Icon, { UploadOutlined } from "@ant-design/icons";
|
import Icon, { UploadOutlined } from "@ant-design/icons";
|
||||||
import { useApolloClient } from "@apollo/client/react";
|
import { useApolloClient } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd";
|
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useLazyQuery, useQuery } from "@apollo/client/react";
|
import { useLazyQuery, useQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
|
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
|
import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -36,6 +36,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
||||||
const firstFieldRefs = useRef({});
|
const firstFieldRefs = useRef({});
|
||||||
|
const lineDescriptionRefs = useRef({});
|
||||||
|
|
||||||
const CONTROL_HEIGHT = 32;
|
const CONTROL_HEIGHT = 32;
|
||||||
|
|
||||||
@@ -94,6 +95,23 @@ export function BillEnterModalLinesComponent({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const focusLineDescription = (index) => {
|
||||||
|
const lineDescription = lineDescriptionRefs.current[index];
|
||||||
|
|
||||||
|
if (typeof lineDescription?.focus === "function") {
|
||||||
|
lineDescription.focus({ preventScroll: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineDescription?.resizableTextArea?.textArea?.focus?.({ preventScroll: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const focusJobLineSelect = (index) => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
firstFieldRefs.current[index]?.focus?.({ preventScroll: true });
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
// Only fill actual_cost when the user forward-tabs out of Retail (actual_price)
|
// Only fill actual_cost when the user forward-tabs out of Retail (actual_price)
|
||||||
const autofillActualCost = (index) => {
|
const autofillActualCost = (index) => {
|
||||||
if (bodyshop.accountingconfig?.disableBillCostCalculation) return;
|
if (bodyshop.accountingconfig?.disableBillCostCalculation) return;
|
||||||
@@ -195,6 +213,12 @@ export function BillEnterModalLinesComponent({
|
|||||||
minHeight: `${CONTROL_HEIGHT}px`
|
minHeight: `${CONTROL_HEIGHT}px`
|
||||||
}}
|
}}
|
||||||
allowRemoved={form.getFieldValue("is_credit_memo") || false}
|
allowRemoved={form.getFieldValue("is_credit_memo") || false}
|
||||||
|
onInputKeyDown={(event) => {
|
||||||
|
if (event.key !== "Tab" || event.shiftKey || event.defaultPrevented) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
focusLineDescription(index);
|
||||||
|
}}
|
||||||
onSelect={(value, opt) => {
|
onSelect={(value, opt) => {
|
||||||
// IMPORTANT:
|
// IMPORTANT:
|
||||||
// Do NOT autofill actual_cost here. It should only fill when the user forward-tabs
|
// Do NOT autofill actual_cost here. It should only fill when the user forward-tabs
|
||||||
@@ -221,6 +245,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
focusJobLineSelect(index);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -236,7 +261,16 @@ export function BillEnterModalLinesComponent({
|
|||||||
label: t("billlines.fields.line_desc"),
|
label: t("billlines.fields.line_desc"),
|
||||||
rules: [{ required: true }]
|
rules: [{ required: true }]
|
||||||
}),
|
}),
|
||||||
formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} />
|
formInput: (record, index) => (
|
||||||
|
<Input.TextArea
|
||||||
|
ref={(el) => {
|
||||||
|
lineDescriptionRefs.current[index] = el;
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
autoSize
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import GlobalSearch from "../global-search/global-search.component";
|
import GlobalSearch from "../global-search/global-search.component";
|
||||||
import GlobalSearchOs from "../global-search/global-search-os.component";
|
import GlobalSearchOs from "../global-search/global-search-os.component";
|
||||||
import "./breadcrumbs.styles.scss";
|
import "./breadcrumbs.styles.scss";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
breadcrumbs: selectBreadcrumbs,
|
breadcrumbs: selectBreadcrumbs,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PictureFilled } from "@ant-design/icons";
|
import { PictureFilled } from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client/react";
|
import { useQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Badge, Popover } from "antd";
|
import { Badge, Popover } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component
|
|||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import { DMS_MAP } from "../../utils/dmsUtils";
|
import { DMS_MAP } from "../../utils/dmsUtils";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CDK-like DMS post form:
|
* CDK-like DMS post form:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import AlertComponent from "../alert/alert.component";
|
|||||||
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
||||||
import JobsDocumentsLocalGalleryExternalComponent from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
import JobsDocumentsLocalGalleryExternalComponent from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
|
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
|||||||
@@ -24,16 +24,28 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export function EsignatureCustomDocument({ bodyshop, jobId, setEsignatureContext }) {
|
export function EsignatureCustomDocument({
|
||||||
|
bodyshop,
|
||||||
|
disabled = false,
|
||||||
|
jobId,
|
||||||
|
setEsignatureContext,
|
||||||
|
showUnavailable = false
|
||||||
|
}) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const esignatureEnabled = hasDocumensoApiKey(bodyshop);
|
||||||
|
const isDisabled = disabled || !esignatureEnabled;
|
||||||
|
|
||||||
if (!hasDocumensoApiKey(bodyshop)) {
|
if (!esignatureEnabled && !showUnavailable) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadCustomDocument = async ({ file, onError, onSuccess }) => {
|
const uploadCustomDocument = async ({ file, onError, onSuccess }) => {
|
||||||
|
if (isDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("document", file);
|
formData.append("document", file);
|
||||||
formData.append("jobid", jobId);
|
formData.append("jobid", jobId);
|
||||||
@@ -78,11 +90,12 @@ export function EsignatureCustomDocument({ bodyshop, jobId, setEsignatureContext
|
|||||||
return Upload.LIST_IGNORE;
|
return Upload.LIST_IGNORE;
|
||||||
}}
|
}}
|
||||||
customRequest={uploadCustomDocument}
|
customRequest={uploadCustomDocument}
|
||||||
|
disabled={isDisabled}
|
||||||
maxCount={1}
|
maxCount={1}
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
>
|
>
|
||||||
<Button icon={<UploadOutlined />} loading={loading}>
|
<Button disabled={isDisabled} icon={<UploadOutlined />} loading={loading}>
|
||||||
{t("esignature.actions.upload_document")}
|
{t("esignature.actions.upload_document")}
|
||||||
</Button>
|
</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
|
|||||||
const { open, context } = esignatureModal;
|
const { open, context } = esignatureModal;
|
||||||
const { token, envelopeId, documentId, jobid } = context;
|
const { token, envelopeId, documentId, jobid } = context;
|
||||||
const [distributing, setDistributing] = useState(false);
|
const [distributing, setDistributing] = useState(false);
|
||||||
|
const hasToken = Boolean(token);
|
||||||
|
|
||||||
if (!hasDocumensoApiKey(bodyshop)) {
|
if (!hasDocumensoApiKey(bodyshop)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -39,6 +40,10 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
|
|||||||
rome: t("jobs.labels.esignature_rome")
|
rome: t("jobs.labels.esignature_rome")
|
||||||
})}
|
})}
|
||||||
onOk={async () => {
|
onOk={async () => {
|
||||||
|
if (!hasToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setDistributing(true);
|
setDistributing(true);
|
||||||
await axios.post("/esign/distribute", {
|
await axios.post("/esign/distribute", {
|
||||||
@@ -58,6 +63,11 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
|
|||||||
setDistributing(false);
|
setDistributing(false);
|
||||||
}}
|
}}
|
||||||
onCancel={async () => {
|
onCancel={async () => {
|
||||||
|
if (!hasToken) {
|
||||||
|
toggleModalVisible();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.post("/esign/delete", {
|
await axios.post("/esign/delete", {
|
||||||
documentId,
|
documentId,
|
||||||
@@ -73,13 +83,15 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
okButtonProps={{ loading: distributing }}
|
okButtonProps={{ loading: distributing, style: hasToken ? undefined : { display: "none" } }}
|
||||||
okText={t("esignature.actions.distribute")}
|
okText={t("esignature.actions.distribute")}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
width={"80%"}
|
width="calc(100vw - 32px)"
|
||||||
|
wrapClassName="esignature-modal"
|
||||||
|
styles={{ body: { overflow: "hidden", padding: 0 } }}
|
||||||
>
|
>
|
||||||
<div style={{ height: "80vh", width: "100%" }}>
|
<div className="esignature-modal-frame">
|
||||||
{token ? (
|
{hasToken ? (
|
||||||
<EmbedUpdateDocumentV1
|
<EmbedUpdateDocumentV1
|
||||||
presignToken={token}
|
presignToken={token}
|
||||||
host="https://sign.imex.online"
|
host="https://sign.imex.online"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { connect } from "react-redux";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx";
|
import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx";
|
||||||
|
import { useTreatment } from "../../feature-flags/splitio-react-replacement.jsx";
|
||||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||||
|
|
||||||
@@ -16,6 +17,12 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
|
|
||||||
export function GlobalFooter({ isPartsEntry }) {
|
export function GlobalFooter({ isPartsEntry }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const testFlagTreatment = useTreatment({ name: "TEST_FLAG" });
|
||||||
|
const testFlagEnabled = testFlagTreatment === "on";
|
||||||
|
|
||||||
|
const testFlagIndicator = testFlagEnabled ? (
|
||||||
|
<div style={{ fontWeight: 600, marginTop: 4 }}>Test Feature Flag Enabled</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
if (isPartsEntry) {
|
if (isPartsEntry) {
|
||||||
return (
|
return (
|
||||||
@@ -38,6 +45,7 @@ export function GlobalFooter({ isPartsEntry }) {
|
|||||||
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
||||||
Disclaimer & Notices
|
Disclaimer & Notices
|
||||||
</Link>
|
</Link>
|
||||||
|
{testFlagIndicator}
|
||||||
</div>
|
</div>
|
||||||
</Footer>
|
</Footer>
|
||||||
);
|
);
|
||||||
@@ -74,6 +82,7 @@ export function GlobalFooter({ isPartsEntry }) {
|
|||||||
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
||||||
Disclaimer & Notices
|
Disclaimer & Notices
|
||||||
</Link>
|
</Link>
|
||||||
|
{testFlagIndicator}
|
||||||
</div>
|
</div>
|
||||||
</Footer>
|
</Footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { BellFilled } from "@ant-design/icons";
|
import { BellFilled } from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client/react";
|
import { useQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Badge, Layout, Menu, Spin, Tooltip } from "antd";
|
import { Badge, Layout, Menu, Spin, Tooltip } from "antd";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from "@apollo/client/react";
|
import { useQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries";
|
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useApolloClient } from "@apollo/client/react";
|
import { useApolloClient } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Popconfirm } from "antd";
|
import { Button, Popconfirm } from "antd";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
|
|||||||
// import AllocationsAssignmentContainer from "../allocations-assignment/allocations-assignment.container";
|
// import AllocationsAssignmentContainer from "../allocations-assignment/allocations-assignment.container";
|
||||||
// import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
|
// import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
|
||||||
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { FaTasks } from "react-icons/fa";
|
import { FaTasks } from "react-icons/fa";
|
||||||
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ export function JobLineDispatchButton({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//joblineids: selectedLines.map((l) => l.id),
|
//joblineids: selectedLines.map((l) => l.id),
|
||||||
}
|
},
|
||||||
|
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
|
||||||
|
awaitRefetchQueries: true
|
||||||
});
|
});
|
||||||
if (result.errors) {
|
if (result.errors) {
|
||||||
console.log("🚀 ~ handleConvert ~ result.errors:", result.errors);
|
console.log("🚀 ~ handleConvert ~ result.errors:", result.errors);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Form, Input, InputNumber, Modal, Select, Switch } from "antd";
|
import { Form, Input, InputNumber, Modal, Select, Switch } from "antd";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation } from "@apollo/client/react";
|
import { useMutation } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import DataLabel from "../data-label/data-label.component";
|
|||||||
import PaymentExpandedRowComponent from "../payment-expanded-row/payment-expanded-row.component";
|
import PaymentExpandedRowComponent from "../payment-expanded-row/payment-expanded-row.component";
|
||||||
import PaymentsGenerateLink from "../payments-generate-link/payments-generate-link.component";
|
import PaymentsGenerateLink from "../payments-generate-link/payments-generate-link.component";
|
||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ADD_JOB_WATCHER, GET_JOB_WATCHERS, REMOVE_JOB_WATCHER } from "../../gra
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
||||||
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useApolloClient, useLazyQuery, useMutation, useQuery } from "@apollo/client/react";
|
import { useApolloClient, useLazyQuery, useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { gql } from "@apollo/client";
|
import { gql } from "@apollo/client";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Col, Row } from "antd";
|
import { Col, Row } from "antd";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useCallback, useMemo, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
|
import { CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DownCircleFilled } from "@ant-design/icons";
|
import { DownCircleFilled } from "@ant-design/icons";
|
||||||
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Card, Dropdown, Form, Input, Modal, Popover, Select, Space } from "antd";
|
import { Button, Card, Dropdown, Form, Input, Modal, Popover, Select, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import LaborAllocationsTableComponent from "../labor-allocations-table/labor-all
|
|||||||
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
||||||
import PayrollLaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.payroll.component";
|
import PayrollLaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.payroll.component";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import cleanAxios from "../../utils/CleanAxios";
|
import cleanAxios from "../../utils/CleanAxios";
|
||||||
import formatBytes from "../../utils/formatbytes";
|
import formatBytes from "../../utils/formatbytes";
|
||||||
//import yauzl from "yauzl";
|
//import yauzl from "yauzl";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import JobDocuments from "./jobs-documents-gallery.component";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const LockWrapper = ({ featureName, bodyshop, children, disabled = true, bypass }) => {
|
const LockWrapper = ({ featureName, bodyshop, children, disabled = true, bypass, locked }) => {
|
||||||
let renderedChildren = children;
|
let renderedChildren = children;
|
||||||
|
|
||||||
//Mark the child prop as disabled.
|
//Mark the child prop as disabled.
|
||||||
@@ -36,11 +36,13 @@ const LockWrapper = ({ featureName, bodyshop, children, disabled = true, bypass
|
|||||||
return <span>{children}</span>;
|
return <span>{children}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return HasFeatureAccess({ featureName: featureName, bodyshop }) ? (
|
const hasAccess = typeof locked === "boolean" ? !locked : HasFeatureAccess({ featureName: featureName, bodyshop });
|
||||||
|
|
||||||
|
return hasAccess ? (
|
||||||
children
|
children
|
||||||
) : (
|
) : (
|
||||||
<Space>
|
<Space>
|
||||||
{!HasFeatureAccess({ featureName: featureName, bodyshop }) && <LockOutlined style={{ color: "tomato" }} />}
|
<LockOutlined style={{ color: "tomato" }} />
|
||||||
{renderedChildren}
|
{renderedChildren}
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
import { Button, Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { TemplateList } from "../../utils/TemplateConstants";
|
|||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import PartsOrderModalComponent from "./parts-order-modal.component";
|
import PartsOrderModalComponent from "./parts-order-modal.component";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Form, Input, Radio, Select } from "antd";
|
import { Form, Input, Radio, Select } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Card, Col, Row, Space, Tooltip, Typography } from "antd";
|
import { Button, Card, Col, Row, Space, Tooltip, Typography } from "antd";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Card, Col, Input, Row, Space, Typography } from "antd";
|
import { CloseOutlined } from "@ant-design/icons";
|
||||||
|
import { Alert, Button, Card, Col, Input, Row, Space, Typography, Tooltip } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -10,12 +11,14 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import Jobd3RdPartyModal from "../job-3rd-party-modal/job-3rd-party-modal.component";
|
import Jobd3RdPartyModal from "../job-3rd-party-modal/job-3rd-party-modal.component";
|
||||||
import EsignatureCustomDocument from "../esignature-custom-document/esignature-custom-document.component";
|
import EsignatureCustomDocument from "../esignature-custom-document/esignature-custom-document.component";
|
||||||
|
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||||
import PrintCenterItem from "../print-center-item/print-center-item.component";
|
import PrintCenterItem from "../print-center-item/print-center-item.component";
|
||||||
import PrintCenterJobsLabels from "../print-center-jobs-labels/print-center-jobs-labels.component";
|
import PrintCenterJobsLabels from "../print-center-jobs-labels/print-center-jobs-labels.component";
|
||||||
import PrintCenterSpeedPrint from "../print-center-speed-print/print-center-speed-print.component";
|
import PrintCenterSpeedPrint from "../print-center-speed-print/print-center-speed-print.component";
|
||||||
import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils";
|
import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils";
|
||||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
import { hasDocumensoApiKey } from "../../utils/esignature.js";
|
import { hasDocumensoApiKey } from "../../utils/esignature.js";
|
||||||
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
printCenterModal: selectPrintCenter,
|
printCenterModal: selectPrintCenter,
|
||||||
@@ -27,6 +30,10 @@ const mapDispatchToProps = () => ({});
|
|||||||
|
|
||||||
export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technician }) {
|
export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technician }) {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const [esignatureBannerDismissed, setEsignatureBannerDismissed] = useLocalStorage(
|
||||||
|
"print_center_esignature_banner_dismissed",
|
||||||
|
false
|
||||||
|
);
|
||||||
const { id: jobId, job } = printCenterModal.context;
|
const { id: jobId, job } = printCenterModal.context;
|
||||||
const tempList = TemplateList("job", {});
|
const tempList = TemplateList("job", {});
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -41,6 +48,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
|
|||||||
const dmsMode = getDmsMode(bodyshop, "off");
|
const dmsMode = getDmsMode(bodyshop, "off");
|
||||||
const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
|
const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
|
||||||
const esignatureEnabled = hasDocumensoApiKey(bodyshop);
|
const esignatureEnabled = hasDocumensoApiKey(bodyshop);
|
||||||
|
const showEsignatureBanner = !esignatureEnabled && !esignatureBannerDismissed;
|
||||||
|
|
||||||
const Templates = !hasDMSKey
|
const Templates = !hasDMSKey
|
||||||
? Object.keys(tempList)
|
? Object.keys(tempList)
|
||||||
@@ -50,7 +58,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
|
|||||||
.filter(
|
.filter(
|
||||||
(temp) =>
|
(temp) =>
|
||||||
(!temp.regions ||
|
(!temp.regions ||
|
||||||
(temp.regions && temp.regions[bodyshop.region_config]) ||
|
temp.regions?.[bodyshop.region_config] ||
|
||||||
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)) &&
|
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)) &&
|
||||||
(!temp.dms || temp.dms === false)
|
(!temp.dms || temp.dms === false)
|
||||||
)
|
)
|
||||||
@@ -62,7 +70,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
|
|||||||
.filter(
|
.filter(
|
||||||
(temp) =>
|
(temp) =>
|
||||||
!temp.regions ||
|
!temp.regions ||
|
||||||
(temp.regions && temp.regions[bodyshop.region_config]) ||
|
temp.regions?.[bodyshop.region_config] ||
|
||||||
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)
|
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)
|
||||||
)
|
)
|
||||||
.filter((temp) => !isReynoldsMode || !temp.excludedDmsModes?.includes(dmsMode))
|
.filter((temp) => !isReynoldsMode || !temp.excludedDmsModes?.includes(dmsMode))
|
||||||
@@ -91,6 +99,23 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{showEsignatureBanner && (
|
||||||
|
<Alert
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
aria-label={t("general.actions.close")}
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={() => setEsignatureBannerDismissed(true)}
|
||||||
|
size="small"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
banner
|
||||||
|
title={t("printcenter.banners.esignature_promo")}
|
||||||
|
type="info"
|
||||||
|
className="print-center-esignature-banner"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col lg={8} md={12} sm={24}>
|
<Col lg={8} md={12} sm={24}>
|
||||||
<PrintCenterSpeedPrint jobId={jobId} />
|
<PrintCenterSpeedPrint jobId={jobId} />
|
||||||
@@ -100,7 +125,13 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
|
|||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<PrintCenterJobsLabels jobId={jobId} />
|
<PrintCenterJobsLabels jobId={jobId} />
|
||||||
{esignatureEnabled && <EsignatureCustomDocument jobId={jobId} />}
|
<Tooltip title={!esignatureEnabled ? t("esignature.tooltips.contact_sales") : null}>
|
||||||
|
<span>
|
||||||
|
<LockWrapperComponent locked={!esignatureEnabled} bodyshop={bodyshop}>
|
||||||
|
<EsignatureCustomDocument jobId={jobId} showUnavailable />
|
||||||
|
</LockWrapperComponent>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
<Jobd3RdPartyModal jobId={jobId} job={job} />
|
<Jobd3RdPartyModal jobId={jobId} job={job} />
|
||||||
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} enterButton />
|
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} enterButton />
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -5,3 +5,7 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.print-center-esignature-banner {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { connect } from "react-redux";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
technician: selectTechnician,
|
technician: selectTechnician,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { UPDATE_ACTIVE_PROD_LIST_VIEW } from "../../graphql/associations.queries";
|
import { UPDATE_ACTIVE_PROD_LIST_VIEW } from "../../graphql/associations.queries";
|
||||||
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
|
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
|
||||||
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
|
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { isFunction } from "lodash";
|
import { isFunction } from "lodash";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { HolderOutlined, SyncOutlined } from "@ant-design/icons";
|
import { HolderOutlined, SyncOutlined } from "@ant-design/icons";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
|
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "../../graphql/jobs.queries";
|
} from "../../graphql/jobs.queries";
|
||||||
import ProductionListTable from "./production-list-table.component";
|
import ProductionListTable from "./production-list-table.component";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
|
||||||
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
|
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useLazyQuery } from "@apollo/client/react";
|
import { useLazyQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography } from "antd";
|
import { Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
|
||||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
@@ -12,11 +12,11 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import {
|
import {
|
||||||
|
CHECK_EMPLOYEE_EMAIL,
|
||||||
CHECK_EMPLOYEE_NUMBER,
|
CHECK_EMPLOYEE_NUMBER,
|
||||||
DELETE_VACATION,
|
DELETE_VACATION,
|
||||||
INSERT_EMPLOYEES,
|
INSERT_EMPLOYEES,
|
||||||
QUERY_EMPLOYEE_BY_ID,
|
QUERY_EMPLOYEE_BY_ID,
|
||||||
QUERY_USERS_BY_EMAIL,
|
|
||||||
UPDATE_EMPLOYEE
|
UPDATE_EMPLOYEE
|
||||||
} from "../../graphql/employees.queries";
|
} from "../../graphql/employees.queries";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
@@ -174,9 +174,10 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi
|
|||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
const submitAction = saveAndResetSubmitAction();
|
const submitAction = saveAndResetSubmitAction();
|
||||||
|
const userEmail = typeof values.user_email === "string" ? values.user_email.trim() : values.user_email;
|
||||||
const normalizedValues = {
|
const normalizedValues = {
|
||||||
...values,
|
...values,
|
||||||
user_email: values.user_email === "" ? null : values.user_email
|
user_email: userEmail === "" ? null : userEmail
|
||||||
};
|
};
|
||||||
|
|
||||||
if (search.employeeId && search.employeeId !== "new") {
|
if (search.employeeId && search.employeeId !== "new") {
|
||||||
@@ -491,18 +492,29 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi
|
|||||||
rules={[
|
rules={[
|
||||||
({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
async validator(rule, value) {
|
async validator(rule, value) {
|
||||||
const user_email = getFieldValue("user_email");
|
const user_email = typeof value === "string" ? value.trim() : getFieldValue("user_email");
|
||||||
|
|
||||||
if (user_email && value) {
|
if (user_email && value) {
|
||||||
const response = await client.query({
|
const response = await client.query({
|
||||||
query: QUERY_USERS_BY_EMAIL,
|
query: CHECK_EMPLOYEE_EMAIL,
|
||||||
variables: {
|
variables: {
|
||||||
email: user_email
|
email: user_email,
|
||||||
|
shopId: bodyshop.id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.users.length === 1) {
|
if (response.data.users.length === 1) {
|
||||||
return Promise.resolve();
|
const matchingEmployees = response.data.employees_aggregate.nodes;
|
||||||
|
const currentEmployeeId = form.getFieldValue("id") ?? search.employeeId;
|
||||||
|
|
||||||
|
if (
|
||||||
|
response.data.employees_aggregate.aggregate.count === 0 ||
|
||||||
|
matchingEmployees.every((employee) => employee.id === currentEmployeeId)
|
||||||
|
) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(t("employees.validation.unique_user_email"));
|
||||||
}
|
}
|
||||||
return Promise.reject(t("bodyshop.validation.useremailmustexist"));
|
return Promise.reject(t("bodyshop.validation.useremailmustexist"));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
CHECK_EMPLOYEE_EMAIL,
|
||||||
DELETE_VACATION,
|
DELETE_VACATION,
|
||||||
INSERT_EMPLOYEES,
|
INSERT_EMPLOYEES,
|
||||||
QUERY_EMPLOYEE_BY_ID,
|
QUERY_EMPLOYEE_BY_ID,
|
||||||
@@ -16,6 +17,7 @@ const updateEmployeeMock = vi.fn();
|
|||||||
const deleteVacationMock = vi.fn();
|
const deleteVacationMock = vi.fn();
|
||||||
const useQueryMock = vi.fn();
|
const useQueryMock = vi.fn();
|
||||||
const useMutationMock = vi.fn();
|
const useMutationMock = vi.fn();
|
||||||
|
const apolloClientQueryMock = vi.fn();
|
||||||
const navigateMock = vi.fn();
|
const navigateMock = vi.fn();
|
||||||
const notification = {
|
const notification = {
|
||||||
error: vi.fn(),
|
error: vi.fn(),
|
||||||
@@ -33,7 +35,7 @@ vi.mock("@apollo/client/react", async () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("@splitsoftware/splitio-react", () => ({
|
vi.mock("../../feature-flags/splitio-react-replacement", () => ({
|
||||||
useTreatmentsWithConfig: () => ({
|
useTreatmentsWithConfig: () => ({
|
||||||
treatments: {
|
treatments: {
|
||||||
Enhanced_Payroll: {
|
Enhanced_Payroll: {
|
||||||
@@ -87,6 +89,10 @@ vi.mock("react-i18next", () => ({
|
|||||||
return "Employee number must be unique";
|
return "Employee number must be unique";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key === "employees.validation.unique_user_email") {
|
||||||
|
return "User email already assigned";
|
||||||
|
}
|
||||||
|
|
||||||
if (key === "bodyshop.validation.useremailmustexist") {
|
if (key === "bodyshop.validation.useremailmustexist") {
|
||||||
return "User email must exist";
|
return "User email must exist";
|
||||||
}
|
}
|
||||||
@@ -203,18 +209,20 @@ describe("ShopEmployeesFormComponent", () => {
|
|||||||
return [vi.fn()];
|
return [vi.fn()];
|
||||||
});
|
});
|
||||||
|
|
||||||
useApolloClient.mockReturnValue({
|
apolloClientQueryMock.mockResolvedValue({
|
||||||
query: vi.fn().mockResolvedValue({
|
data: {
|
||||||
data: {
|
employees_aggregate: {
|
||||||
employees_aggregate: {
|
aggregate: {
|
||||||
aggregate: {
|
count: 0
|
||||||
count: 0
|
|
||||||
},
|
|
||||||
nodes: []
|
|
||||||
},
|
},
|
||||||
users: []
|
nodes: []
|
||||||
}
|
},
|
||||||
})
|
users: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useApolloClient.mockReturnValue({
|
||||||
|
query: apolloClientQueryMock
|
||||||
});
|
});
|
||||||
|
|
||||||
insertEmployeesMock.mockResolvedValue({
|
insertEmployeesMock.mockResolvedValue({
|
||||||
@@ -356,4 +364,59 @@ describe("ShopEmployeesFormComponent", () => {
|
|||||||
title: "Saved"
|
title: "Saved"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("blocks saving when the user email belongs to another employee in the shop", async () => {
|
||||||
|
apolloClientQueryMock.mockImplementation(({ query }) => {
|
||||||
|
if (query === CHECK_EMPLOYEE_EMAIL) {
|
||||||
|
return Promise.resolve({
|
||||||
|
data: {
|
||||||
|
users: [{ email: "jamie@example.com" }],
|
||||||
|
employees_aggregate: {
|
||||||
|
aggregate: {
|
||||||
|
count: 1
|
||||||
|
},
|
||||||
|
nodes: [{ id: "other-employee" }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve({
|
||||||
|
data: {
|
||||||
|
employees_aggregate: {
|
||||||
|
aggregate: {
|
||||||
|
count: 0
|
||||||
|
},
|
||||||
|
nodes: []
|
||||||
|
},
|
||||||
|
users: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "First Name" }), {
|
||||||
|
target: { value: "Jamie" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Last Name" }), {
|
||||||
|
target: { value: "Rivera" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Employee Number" }), {
|
||||||
|
target: { value: "42" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "PIN" }), {
|
||||||
|
target: { value: "1234" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "Hire Date" }), {
|
||||||
|
target: { value: "2026-04-20" }
|
||||||
|
});
|
||||||
|
fireEvent.change(screen.getByRole("textbox", { name: "User Email" }), {
|
||||||
|
target: { value: "jamie@example.com" }
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Save Employee" }));
|
||||||
|
|
||||||
|
expect(await screen.findByText("User email already assigned")).toBeInTheDocument();
|
||||||
|
expect(insertEmployeesMock).not.toHaveBeenCalled();
|
||||||
|
expect(notification.success).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Card, Tabs } from "antd";
|
import { Button, Card, Tabs } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Form, InputNumber } from "antd";
|
import { Form, InputNumber } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Col, DatePicker, Divider, Form, Input, InputNumber, Radio, Row, Select, Space, Switch } from "antd";
|
import { Button, Col, DatePicker, Divider, Form, Input, InputNumber, Radio, Row, Select, Space, Switch } from "antd";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { DEFAULT_TRANSLUCENT_CARD_COLOR, getTintedCardSurfaceStyles } from "./sh
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||||
import JobsDetailLaborContainer from "../jobs-detail-labor/jobs-detail-labor.container";
|
import JobsDetailLaborContainer from "../jobs-detail-labor/jobs-detail-labor.container";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
|
|||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
|
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
|
||||||
import TechClockInComponent from "./tech-job-clock-in-form.component";
|
import TechClockInComponent from "./tech-job-clock-in-form.component";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
|
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
|
||||||
import TechJobClockoutDelete from "../tech-job-clock-out-delete/tech-job-clock-out-delete.component";
|
import TechJobClockoutDelete from "../tech-job-clock-out-delete/tech-job-clock-out-delete.component";
|
||||||
import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
|
import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { techLogout } from "../../redux/tech/tech.actions";
|
import { techLogout } from "../../redux/tech/tech.actions";
|
||||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
import { BsKanban } from "react-icons/bs";
|
import { BsKanban } from "react-icons/bs";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Card, Checkbox, Space } from "antd";
|
import { Button, Card, Checkbox, Space } from "antd";
|
||||||
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useLazyQuery } from "@apollo/client/react";
|
import { useLazyQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Card, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
import { Card, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import { useMutation, useQuery } from "@apollo/client/react";
|
import { useMutation, useQuery } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Form, Modal, Space } from "antd";
|
import { Button, Form, Modal, Space } from "antd";
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useApolloClient } from "@apollo/client/react";
|
import { useApolloClient } from "@apollo/client/react";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from "../../graphql/notifications.queries.js";
|
} from "../../graphql/notifications.queries.js";
|
||||||
import { useMutation } from "@apollo/client/react";
|
import { useMutation } from "@apollo/client/react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { FEATURE_FLAGS_CHANGED_EVENT, useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { INITIAL_NOTIFICATIONS, SocketContext } from "./useSocket.js";
|
import { INITIAL_NOTIFICATIONS, SocketContext } from "./useSocket.js";
|
||||||
|
|
||||||
const LIMIT = INITIAL_NOTIFICATIONS;
|
const LIMIT = INITIAL_NOTIFICATIONS;
|
||||||
@@ -280,6 +280,10 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFeatureFlagsChanged = (payload) => {
|
||||||
|
window.dispatchEvent(new CustomEvent(FEATURE_FLAGS_CHANGED_EVENT, { detail: payload }));
|
||||||
|
};
|
||||||
|
|
||||||
const syncCurrentTokenToSocket = async () => {
|
const syncCurrentTokenToSocket = async () => {
|
||||||
try {
|
try {
|
||||||
if (!auth.currentUser || !bodyshop?.id) return;
|
if (!auth.currentUser || !bodyshop?.id) return;
|
||||||
@@ -574,6 +578,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
|||||||
socketInstance.on("notification", handleNotification);
|
socketInstance.on("notification", handleNotification);
|
||||||
socketInstance.on("sync-notification-read", handleSyncNotificationRead);
|
socketInstance.on("sync-notification-read", handleSyncNotificationRead);
|
||||||
socketInstance.on("sync-all-notifications-read", handleSyncAllNotificationsRead);
|
socketInstance.on("sync-all-notifications-read", handleSyncAllNotificationsRead);
|
||||||
|
socketInstance.on(FEATURE_FLAGS_CHANGED_EVENT, handleFeatureFlagsChanged);
|
||||||
socketInstance.on("token-updated", handleTokenUpdated);
|
socketInstance.on("token-updated", handleTokenUpdated);
|
||||||
|
|
||||||
if (tokenSyncIntervalRef.current) {
|
if (tokenSyncIntervalRef.current) {
|
||||||
|
|||||||
71
client/src/feature-flags/README.md
Normal file
71
client/src/feature-flags/README.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Feature Flags
|
||||||
|
|
||||||
|
The app imports feature-flag hooks from `src/feature-flags/splitio-react-replacement.jsx`. That module keeps the old
|
||||||
|
Split-shaped component and hook API intact while removing the runtime dependency on Split.
|
||||||
|
|
||||||
|
Code should import this local module directly. We no longer rely on a Vite alias for the old Split package.
|
||||||
|
|
||||||
|
## Current storage contract
|
||||||
|
|
||||||
|
The compatibility layer reads the active shop from Redux, then fetches DB-backed assignments from:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /feature-flags/bodyshops/:bodyshopId
|
||||||
|
```
|
||||||
|
|
||||||
|
That endpoint verifies the Firebase user can access the bodyshop through Hasura permissions, then returns cached Redis
|
||||||
|
data when present or refreshes from `feature_flags` + `bodyshop_feature_flags`.
|
||||||
|
|
||||||
|
On successful backend responses, the client stores the last-known flag payload in browser `localStorage` for the active
|
||||||
|
bodyshop. If the backend cannot be reached later, the client uses that bodyshop-scoped browser cache for up to 24 hours.
|
||||||
|
If there is no browser cache, unknown flags resolve to `"off"`.
|
||||||
|
|
||||||
|
Recommended backend payload shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"flags": {
|
||||||
|
"Enhanced_Payroll": {
|
||||||
|
"treatment": "on",
|
||||||
|
"config": null,
|
||||||
|
"activeDate": null,
|
||||||
|
"deactiveDate": null
|
||||||
|
},
|
||||||
|
"Demo_Feature": {
|
||||||
|
"treatment": "on",
|
||||||
|
"config": null,
|
||||||
|
"activeDate": "2026-06-01T13:00:00-04:00",
|
||||||
|
"deactiveDate": "2026-06-05T17:00:00-04:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported values:
|
||||||
|
|
||||||
|
- `true`, `"true"`, `1`, `"on"` -> treatment `"on"`
|
||||||
|
- `false`, `"false"`, `0`, `"off"` -> treatment `"off"`
|
||||||
|
- ISO-ish future date strings -> `"on"` until the date passes
|
||||||
|
- `{ "treatment": "on" | "off" | "control" | "any-custom-treatment", "config": ... }`
|
||||||
|
- Scheduled demo windows using `activeDate` and `deactiveDate`
|
||||||
|
|
||||||
|
Unknown flags default to `"off"`.
|
||||||
|
|
||||||
|
## Backend registry
|
||||||
|
|
||||||
|
Canonical feature flag definitions live in the Hasura-backed `feature_flags` table and are exposed to the admin panel
|
||||||
|
through `GET /adm/feature-flags`.
|
||||||
|
|
||||||
|
Per-shop assignments live in `bodyshop_feature_flags`. The admin panel reads them through
|
||||||
|
`GET /adm/bodyshops/:bodyshopId/feature-flags` and saves them through `POST /adm/updateshop`.
|
||||||
|
|
||||||
|
Hasura invalidates the Redis cache through `/feature-flags/cache/invalidate` when `bodyshop_feature_flags` or
|
||||||
|
`feature_flags` changes. Assignment changes clear the affected shop cache for the current cache version; definition
|
||||||
|
changes increment a global feature flag cache version so old per-shop cache entries become invisible and expire by TTL.
|
||||||
|
|
||||||
|
The backend also emits `feature-flags-changed` over the existing Socket.IO connection. `SocketProvider` bridges that
|
||||||
|
socket message to a browser event, and `SplitFactoryProvider` refetches flags when the event is global or matches the
|
||||||
|
active bodyshop. This keeps already-open tabs in sync with admin edits and Hasura-triggered invalidation.
|
||||||
|
|
||||||
|
For manual frontend testing, the global footer displays `Test Feature Flag Enabled` when `TEST_FLAG` resolves to
|
||||||
|
the `on` treatment.
|
||||||
411
client/src/feature-flags/splitio-react-replacement.jsx
Normal file
411
client/src/feature-flags/splitio-react-replacement.jsx
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { selectBodyshop } from "../redux/user/user.selectors";
|
||||||
|
|
||||||
|
const FeatureFlagContext = createContext({
|
||||||
|
config: {},
|
||||||
|
factory: null,
|
||||||
|
flags: {},
|
||||||
|
isReady: true,
|
||||||
|
source: "local"
|
||||||
|
});
|
||||||
|
|
||||||
|
const OFF_TREATMENT = Object.freeze({ treatment: "off", config: null });
|
||||||
|
const LOCAL_STORAGE_PREFIX = "bodyshop-feature-flags";
|
||||||
|
const LOCAL_STORAGE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
||||||
|
const FEATURE_FLAGS_REFRESH_DEBOUNCE_MS = 150;
|
||||||
|
const MAX_SCHEDULE_REFRESH_DELAY_MS = 2_147_483_647;
|
||||||
|
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
||||||
|
const hasSchedule = (value) => value.activeDate != null || value.deactiveDate != null;
|
||||||
|
|
||||||
|
export const FEATURE_FLAGS_CHANGED_EVENT = "feature-flags-changed";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses optional schedule timestamps into comparable epoch milliseconds.
|
||||||
|
*/
|
||||||
|
const parseDate = (value) => {
|
||||||
|
if (value == null || value === "") return null;
|
||||||
|
const timestamp = Date.parse(value);
|
||||||
|
return Number.isNaN(timestamp) ? null : timestamp;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a scheduled feature flag assignment is active at the current time.
|
||||||
|
*/
|
||||||
|
const isWithinSchedule = (value) => {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return true;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const startsAt = parseDate(value.activeDate);
|
||||||
|
const endsAt = parseDate(value.deactiveDate);
|
||||||
|
|
||||||
|
if (startsAt != null && now < startsAt) return false;
|
||||||
|
if (endsAt != null && now >= endsAt) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes backend config values into the object/string/null shape Split hooks expect.
|
||||||
|
*/
|
||||||
|
const normalizeConfig = (config) => {
|
||||||
|
if (config == null || config === "") return null;
|
||||||
|
if (typeof config === "string") {
|
||||||
|
try {
|
||||||
|
return JSON.parse(config);
|
||||||
|
} catch {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts legacy boolean-ish values and custom treatment strings into a stable treatment value.
|
||||||
|
*/
|
||||||
|
const normalizeTreatment = (value) => {
|
||||||
|
if (typeof value === "boolean") return value ? "on" : "off";
|
||||||
|
if (typeof value === "number") return value > 0 ? "on" : "off";
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const normalized = value.trim();
|
||||||
|
const lowered = normalized.toLowerCase();
|
||||||
|
|
||||||
|
if (lowered === "true") return "on";
|
||||||
|
if (lowered === "false") return "off";
|
||||||
|
if (lowered === "on" || lowered === "off" || lowered === "control") return lowered;
|
||||||
|
|
||||||
|
const dateValue = Date.parse(normalized);
|
||||||
|
if (!Number.isNaN(dateValue)) return dateValue > Date.now() ? "on" : "off";
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value ? "on" : "off";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts any supported backend flag value into a Split-compatible treatment/config pair.
|
||||||
|
*/
|
||||||
|
const normalizeFlagValue = (value) => {
|
||||||
|
if (value == null) return OFF_TREATMENT;
|
||||||
|
|
||||||
|
if (typeof value === "object" && !Array.isArray(value)) {
|
||||||
|
if (!isWithinSchedule(value)) return OFF_TREATMENT;
|
||||||
|
|
||||||
|
if (hasOwn(value, "treatment")) {
|
||||||
|
return {
|
||||||
|
treatment: normalizeTreatment(value.treatment),
|
||||||
|
config: normalizeConfig(value.config)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOwn(value, "enabled")) {
|
||||||
|
return {
|
||||||
|
treatment: normalizeTreatment(value.enabled),
|
||||||
|
config: normalizeConfig(value.config)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSchedule(value)) {
|
||||||
|
return {
|
||||||
|
treatment: "on",
|
||||||
|
config: normalizeConfig(value.config)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
treatment: normalizeTreatment(value),
|
||||||
|
config: null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a socket/browser feature-flag change event applies to the active bodyshop.
|
||||||
|
*/
|
||||||
|
const isFeatureFlagChangeRelevant = (detail, bodyshopId) => {
|
||||||
|
if (!detail || detail.scope === "global") return true;
|
||||||
|
if (!bodyshopId) return false;
|
||||||
|
return String(detail.bodyshopId) === String(bodyshopId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the next scheduled flag boundary that should force a local re-render.
|
||||||
|
*/
|
||||||
|
const getNextScheduleRefreshDelay = (flags = {}, now = Date.now()) => {
|
||||||
|
const nextTimestamp = Object.values(flags).reduce((next, value) => {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) return next;
|
||||||
|
|
||||||
|
const timestamps = [parseDate(value.activeDate), parseDate(value.deactiveDate)].filter(
|
||||||
|
(timestamp) => timestamp != null && timestamp > now
|
||||||
|
);
|
||||||
|
if (!timestamps.length) return next;
|
||||||
|
|
||||||
|
const candidate = Math.min(...timestamps);
|
||||||
|
return next == null ? candidate : Math.min(next, candidate);
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
if (nextTimestamp == null) return null;
|
||||||
|
|
||||||
|
return Math.min(Math.max(nextTimestamp - now + 50, 0), MAX_SCHEDULE_REFRESH_DELAY_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether browser localStorage can be used in the current runtime.
|
||||||
|
*/
|
||||||
|
const isBrowserStorageAvailable = () => typeof window !== "undefined" && window.localStorage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the browser cache key for one bodyshop's feature flags.
|
||||||
|
*/
|
||||||
|
const getLocalStorageKey = (bodyshopId) => `${LOCAL_STORAGE_PREFIX}:${bodyshopId}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a bodyshop-scoped last-known-good flag payload from browser storage.
|
||||||
|
*/
|
||||||
|
const readCachedFeatureFlags = (bodyshopId, now = Date.now()) => {
|
||||||
|
if (!bodyshopId || !isBrowserStorageAvailable()) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawValue = window.localStorage.getItem(getLocalStorageKey(bodyshopId));
|
||||||
|
if (!rawValue) return null;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(rawValue);
|
||||||
|
if (!parsed?.flags || typeof parsed.flags !== "object" || Array.isArray(parsed.flags)) return null;
|
||||||
|
const cachedAt = Date.parse(parsed.cachedAt);
|
||||||
|
if (!parsed.cachedAt || Number.isNaN(cachedAt) || now - cachedAt > LOCAL_STORAGE_MAX_AGE_MS) return null;
|
||||||
|
|
||||||
|
return parsed.flags;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a successful backend flag payload for short-term browser fallback.
|
||||||
|
*/
|
||||||
|
const writeCachedFeatureFlags = (bodyshopId, flags) => {
|
||||||
|
if (!bodyshopId || !flags || !isBrowserStorageAvailable()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
getLocalStorageKey(bodyshopId),
|
||||||
|
JSON.stringify({
|
||||||
|
cachedAt: new Date().toISOString(),
|
||||||
|
flags
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// localStorage may be unavailable, full, or blocked. Runtime flags still work without the browser cache.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the local client object that mimics the Split client surface used by the app.
|
||||||
|
*/
|
||||||
|
const createFeatureFlagClient = ({ bodyshop, key, backendFlags }) => {
|
||||||
|
const attributes = {};
|
||||||
|
|
||||||
|
const getTreatmentWithConfig = (name) => normalizeFlagValue(backendFlags?.[name]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
client: null,
|
||||||
|
isReady: true,
|
||||||
|
isReadyFromCache: true,
|
||||||
|
key: key || bodyshop?.imexshopid || "anon",
|
||||||
|
getTreatment: (name) => getTreatmentWithConfig(name).treatment,
|
||||||
|
getTreatmentWithConfig,
|
||||||
|
getTreatments: (names = []) =>
|
||||||
|
names.reduce((acc, name) => {
|
||||||
|
acc[name] = getTreatmentWithConfig(name).treatment;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
getTreatmentsWithConfig: (names = []) =>
|
||||||
|
names.reduce((acc, name) => {
|
||||||
|
acc[name] = getTreatmentWithConfig(name);
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
setAttribute: (name, value) => {
|
||||||
|
attributes[name] = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
setAttributes: (values = {}) => {
|
||||||
|
Object.assign(attributes, values);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
getAttribute: (name) => attributes[name],
|
||||||
|
getAttributes: () => ({ ...attributes }),
|
||||||
|
ready: () => Promise.resolve(),
|
||||||
|
on: () => {},
|
||||||
|
off: () => {},
|
||||||
|
destroy: () => {}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides database-backed feature flags through a Split-shaped React context.
|
||||||
|
*/
|
||||||
|
export function SplitFactoryProvider({ children, config, factory }) {
|
||||||
|
const bodyshop = useSelector(selectBodyshop);
|
||||||
|
const [state, setState] = useState({ flags: {}, isReady: true, source: "local" });
|
||||||
|
const loadIdRef = useRef(0);
|
||||||
|
const refreshTimerRef = useRef(null);
|
||||||
|
|
||||||
|
const loadFeatureFlags = useCallback(async () => {
|
||||||
|
const loadId = (loadIdRef.current += 1);
|
||||||
|
|
||||||
|
if (!bodyshop?.id) {
|
||||||
|
setState({ flags: {}, isReady: true, source: "local" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((current) => ({ ...current, isReady: false }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`/feature-flags/bodyshops/${bodyshop.id}`);
|
||||||
|
if (loadId !== loadIdRef.current) return;
|
||||||
|
const flags = data.flags || {};
|
||||||
|
writeCachedFeatureFlags(bodyshop.id, flags);
|
||||||
|
setState({
|
||||||
|
flags,
|
||||||
|
isReady: true,
|
||||||
|
source: data.source || "database"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (loadId !== loadIdRef.current) return;
|
||||||
|
const cachedFlags = readCachedFeatureFlags(bodyshop.id);
|
||||||
|
console.warn("Feature flags backend fetch failed; falling back to last-known browser cache.", error);
|
||||||
|
setState({
|
||||||
|
flags: cachedFlags || {},
|
||||||
|
isReady: true,
|
||||||
|
source: cachedFlags ? "browser-cache" : "local"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [bodyshop?.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadFeatureFlags();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
loadIdRef.current += 1;
|
||||||
|
};
|
||||||
|
}, [loadFeatureFlags]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!bodyshop?.id) return undefined;
|
||||||
|
|
||||||
|
const handleFeatureFlagsChanged = (event) => {
|
||||||
|
if (!isFeatureFlagChangeRelevant(event.detail, bodyshop.id)) return;
|
||||||
|
|
||||||
|
if (refreshTimerRef.current) {
|
||||||
|
clearTimeout(refreshTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTimerRef.current = setTimeout(() => {
|
||||||
|
refreshTimerRef.current = null;
|
||||||
|
loadFeatureFlags();
|
||||||
|
}, FEATURE_FLAGS_REFRESH_DEBOUNCE_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener(FEATURE_FLAGS_CHANGED_EVENT, handleFeatureFlagsChanged);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(FEATURE_FLAGS_CHANGED_EVENT, handleFeatureFlagsChanged);
|
||||||
|
if (refreshTimerRef.current) {
|
||||||
|
clearTimeout(refreshTimerRef.current);
|
||||||
|
refreshTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [bodyshop?.id, loadFeatureFlags]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const delay = getNextScheduleRefreshDelay(state.flags);
|
||||||
|
if (delay == null) return undefined;
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setState((current) => ({ ...current, flags: { ...current.flags } }));
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [state.flags]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({ config, factory, flags: state.flags, isReady: state.isReady, source: state.source }),
|
||||||
|
[config, factory, state.flags, state.isReady, state.source]
|
||||||
|
);
|
||||||
|
return <FeatureFlagContext.Provider value={value}>{children}</FeatureFlagContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Split-compatible client backed by the local feature flag context.
|
||||||
|
*/
|
||||||
|
export function useSplitClient(options = {}) {
|
||||||
|
const bodyshop = useSelector(selectBodyshop);
|
||||||
|
const context = useContext(FeatureFlagContext);
|
||||||
|
|
||||||
|
const client = useMemo(() => {
|
||||||
|
const nextClient = createFeatureFlagClient({
|
||||||
|
bodyshop,
|
||||||
|
key: options.key,
|
||||||
|
backendFlags: context.flags
|
||||||
|
});
|
||||||
|
nextClient.client = nextClient;
|
||||||
|
nextClient.isReady = context.isReady;
|
||||||
|
nextClient.isReadyFromCache = context.source === "redis" || context.source === "browser-cache";
|
||||||
|
return nextClient;
|
||||||
|
}, [bodyshop, options.key, context.flags, context.isReady, context.source]);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns treatment/config pairs for several feature flags.
|
||||||
|
*/
|
||||||
|
export function useTreatmentsWithConfig({ names = [] } = {}) {
|
||||||
|
const client = useSplitClient();
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
treatments: client.getTreatmentsWithConfig(names),
|
||||||
|
isReady: client.isReady,
|
||||||
|
isReadyFromCache: client.isReadyFromCache,
|
||||||
|
lastUpdate: Date.now()
|
||||||
|
}),
|
||||||
|
[client, names]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns only the treatment string for one feature flag.
|
||||||
|
*/
|
||||||
|
export function useTreatment({ name } = {}) {
|
||||||
|
const client = useSplitClient();
|
||||||
|
return client.getTreatment(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the treatment/config pair for one feature flag.
|
||||||
|
*/
|
||||||
|
export function useTreatmentWithConfig({ name } = {}) {
|
||||||
|
const client = useSplitClient();
|
||||||
|
return client.getTreatmentWithConfig(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureFlagProvider = SplitFactoryProvider;
|
||||||
|
export const useFeatureFlagClient = useSplitClient;
|
||||||
|
export const SplitContext = FeatureFlagContext;
|
||||||
|
export const useSplitContext = () => useContext(FeatureFlagContext);
|
||||||
|
|
||||||
|
export const __featureFlagTesting = {
|
||||||
|
createFeatureFlagClient,
|
||||||
|
getNextScheduleRefreshDelay,
|
||||||
|
getLocalStorageKey,
|
||||||
|
isFeatureFlagChangeRelevant,
|
||||||
|
normalizeFlagValue,
|
||||||
|
readCachedFeatureFlags,
|
||||||
|
writeCachedFeatureFlags
|
||||||
|
};
|
||||||
166
client/src/feature-flags/splitio-react-replacement.test.jsx
Normal file
166
client/src/feature-flags/splitio-react-replacement.test.jsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { __featureFlagTesting } from "./splitio-react-replacement";
|
||||||
|
|
||||||
|
const {
|
||||||
|
createFeatureFlagClient,
|
||||||
|
getNextScheduleRefreshDelay,
|
||||||
|
getLocalStorageKey,
|
||||||
|
isFeatureFlagChangeRelevant,
|
||||||
|
normalizeFlagValue,
|
||||||
|
readCachedFeatureFlags,
|
||||||
|
writeCachedFeatureFlags
|
||||||
|
} = __featureFlagTesting;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
window.localStorage.clear();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("splitio-react-replacement feature flag normalization", () => {
|
||||||
|
it("returns off for unknown or null values", () => {
|
||||||
|
expect(normalizeFlagValue(null)).toEqual({ treatment: "off", config: null });
|
||||||
|
expect(normalizeFlagValue(undefined)).toEqual({ treatment: "off", config: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes primitive values into Split-like treatments", () => {
|
||||||
|
expect(normalizeFlagValue(true)).toEqual({ treatment: "on", config: null });
|
||||||
|
expect(normalizeFlagValue(false)).toEqual({ treatment: "off", config: null });
|
||||||
|
expect(normalizeFlagValue(1)).toEqual({ treatment: "on", config: null });
|
||||||
|
expect(normalizeFlagValue(0)).toEqual({ treatment: "off", config: null });
|
||||||
|
expect(normalizeFlagValue("true")).toEqual({ treatment: "on", config: null });
|
||||||
|
expect(normalizeFlagValue("false")).toEqual({ treatment: "off", config: null });
|
||||||
|
expect(normalizeFlagValue("variant-a")).toEqual({ treatment: "variant-a", config: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves custom treatments and parses JSON config strings", () => {
|
||||||
|
expect(
|
||||||
|
normalizeFlagValue({
|
||||||
|
treatment: "demo",
|
||||||
|
config: "{\"limit\":25}"
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
treatment: "demo",
|
||||||
|
config: { limit: 25 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects activeDate and deactiveDate windows", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-05-19T15:00:00.000Z"));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
normalizeFlagValue({
|
||||||
|
treatment: "on",
|
||||||
|
activeDate: "2026-05-19T14:59:00.000Z",
|
||||||
|
deactiveDate: "2026-05-19T15:01:00.000Z"
|
||||||
|
})
|
||||||
|
).toEqual({ treatment: "on", config: null });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
normalizeFlagValue({
|
||||||
|
treatment: "on",
|
||||||
|
activeDate: "2026-05-19T15:01:00.000Z"
|
||||||
|
})
|
||||||
|
).toEqual({ treatment: "off", config: null });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
normalizeFlagValue({
|
||||||
|
treatment: "on",
|
||||||
|
deactiveDate: "2026-05-19T15:00:00.000Z"
|
||||||
|
})
|
||||||
|
).toEqual({ treatment: "off", config: null });
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("splitio-react-replacement feature flag client", () => {
|
||||||
|
it("uses backend flags", () => {
|
||||||
|
const client = createFeatureFlagClient({
|
||||||
|
bodyshop: {
|
||||||
|
imexshopid: "APPLE"
|
||||||
|
},
|
||||||
|
backendFlags: {
|
||||||
|
Enhanced_Payroll: { treatment: "on" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(client.getTreatment("Enhanced_Payroll")).toBe("on");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores old bodyshop feature JSON fallback values", () => {
|
||||||
|
const client = createFeatureFlagClient({
|
||||||
|
bodyshop: {
|
||||||
|
imexshopid: "APPLE",
|
||||||
|
features: {
|
||||||
|
featureFlags: {
|
||||||
|
Enhanced_Payroll: { treatment: "on" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backendFlags: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(client.getTreatment("Enhanced_Payroll")).toBe("off");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns off for flags that are not present in any source", () => {
|
||||||
|
const client = createFeatureFlagClient({
|
||||||
|
bodyshop: { imexshopid: "APPLE", features: {} },
|
||||||
|
backendFlags: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(client.getTreatment("Missing_Flag")).toBe("off");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses a bodyshop-scoped browser cache key", () => {
|
||||||
|
expect(getLocalStorageKey("shop-1")).toBe("bodyshop-feature-flags:shop-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores and reads last-known backend flags from browser storage", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-05-19T15:00:00.000Z"));
|
||||||
|
|
||||||
|
writeCachedFeatureFlags("shop-1", {
|
||||||
|
Enhanced_Payroll: { treatment: "on", config: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(readCachedFeatureFlags("shop-1")).toEqual({
|
||||||
|
Enhanced_Payroll: { treatment: "on", config: null }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores expired browser cached flags", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-05-19T15:00:00.000Z"));
|
||||||
|
|
||||||
|
writeCachedFeatureFlags("shop-1", {
|
||||||
|
Enhanced_Payroll: { treatment: "on", config: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(readCachedFeatureFlags("shop-1", Date.parse("2026-05-20T15:00:01.000Z"))).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("splitio-react-replacement live refresh helpers", () => {
|
||||||
|
it("matches global and bodyshop-scoped socket changes", () => {
|
||||||
|
expect(isFeatureFlagChangeRelevant({ scope: "global" }, "shop-1")).toBe(true);
|
||||||
|
expect(isFeatureFlagChangeRelevant({ bodyshopId: "shop-1", scope: "bodyshop" }, "shop-1")).toBe(true);
|
||||||
|
expect(isFeatureFlagChangeRelevant({ bodyshopId: "shop-2", scope: "bodyshop" }, "shop-1")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds the next active/deactive date boundary that needs a refresh", () => {
|
||||||
|
const now = Date.parse("2026-05-19T15:00:00.000Z");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getNextScheduleRefreshDelay(
|
||||||
|
{
|
||||||
|
Demo: { treatment: "on", activeDate: "2026-05-19T15:05:00.000Z" },
|
||||||
|
Expiring: { treatment: "on", deactiveDate: "2026-05-19T15:02:00.000Z" },
|
||||||
|
Expired: { treatment: "on", deactiveDate: "2026-05-19T14:59:00.000Z" }
|
||||||
|
},
|
||||||
|
now
|
||||||
|
)
|
||||||
|
).toBe(120050);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -49,6 +49,22 @@ export const CHECK_EMPLOYEE_NUMBER = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const CHECK_EMPLOYEE_EMAIL = gql`
|
||||||
|
query CHECK_EMPLOYEE_EMAIL($email: String!, $shopId: uuid!) {
|
||||||
|
users(where: { email: { _ilike: $email } }) {
|
||||||
|
email
|
||||||
|
}
|
||||||
|
employees_aggregate(where: { user_email: { _ilike: $email }, shopid: { _eq: $shopId } }) {
|
||||||
|
aggregate {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const QUERY_ACTIVE_EMPLOYEES = gql`
|
export const QUERY_ACTIVE_EMPLOYEES = gql`
|
||||||
query QUERY_ACTIVE_EMPLOYEES {
|
query QUERY_ACTIVE_EMPLOYEES {
|
||||||
employees(where: { active: { _eq: true } }) {
|
employees(where: { active: { _eq: true } }) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { useQuery } from "@apollo/client/react";
|
import { useQuery } from "@apollo/client/react";
|
||||||
import { Button, Card, Col, Result, Row, Select, Space, Switch } from "antd";
|
import { Button, Card, Col, Result, Row, Select, Space, Switch } from "antd";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { some } from "lodash";
|
import { some } from "lodash";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import AlertComponent from "../../components/alert/alert.component";
|
import AlertComponent from "../../components/alert/alert.component";
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
// import { useNavigate } from 'react-router-dom';
|
// import { useNavigate } from 'react-router-dom';
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ProductionBoardKanbanContainer from "../../components/production-board-ka
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import ProductionListTable from "../../components/production-list-table/producti
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { GET_UNACCEPTED_PARTS_DISPATCH } from "../../graphql/parts-dispatch.quer
|
|||||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import JobsDocumentsContainer from "../../components/jobs-documents-gallery/jobs
|
|||||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||||
import { QUERY_TEMPORARY_DOCS } from "../../graphql/documents.queries";
|
import { QUERY_TEMPORARY_DOCS } from "../../graphql/documents.queries";
|
||||||
|
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/appli
|
|||||||
import TimeTicketsCommit from "../../components/time-tickets-commit/time-tickets-commit.component";
|
import TimeTicketsCommit from "../../components/time-tickets-commit/time-tickets-commit.component";
|
||||||
import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wrapper.component";
|
import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wrapper.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component";
|
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
||||||
//import { setUserId, setUserProperties } from "@firebase/analytics";
|
|
||||||
import {
|
import {
|
||||||
checkActionCode,
|
checkActionCode,
|
||||||
confirmPasswordReset,
|
confirmPasswordReset,
|
||||||
@@ -9,11 +8,9 @@ import {
|
|||||||
} from "@firebase/auth";
|
} from "@firebase/auth";
|
||||||
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
||||||
import { getToken } from "@firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
// import * as Sentry from "@sentry/react";
|
|
||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
//import LogRocket from "logrocket";
|
|
||||||
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
||||||
import {
|
import {
|
||||||
auth,
|
auth,
|
||||||
@@ -48,9 +45,13 @@ import {
|
|||||||
validatePasswordResetSuccess
|
validatePasswordResetSuccess
|
||||||
} from "./user.actions";
|
} from "./user.actions";
|
||||||
import UserActionTypes from "./user.types";
|
import UserActionTypes from "./user.types";
|
||||||
//import posthog from "posthog-js";
|
|
||||||
import { bodyshopHasDmsKey, determineDMSTypeByBodyshop, DMS_MAP } from "../../utils/dmsUtils";
|
import { bodyshopHasDmsKey, determineDMSTypeByBodyshop, DMS_MAP } from "../../utils/dmsUtils";
|
||||||
|
|
||||||
|
//import { setUserId, setUserProperties } from "@firebase/analytics";
|
||||||
|
//import * as Sentry from "@sentry/react";
|
||||||
|
//import LogRocket from "logrocket";
|
||||||
|
//import posthog from "posthog-js";
|
||||||
|
|
||||||
const fpPromise = FingerprintJS.load();
|
const fpPromise = FingerprintJS.load();
|
||||||
|
|
||||||
export function* onEmailSignInStart() {
|
export function* onEmailSignInStart() {
|
||||||
|
|||||||
@@ -1351,7 +1351,8 @@
|
|||||||
"vacationadded": "Employee vacation added."
|
"vacationadded": "Employee vacation added."
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"unique_employee_number": "You must enter a unique employee number."
|
"unique_employee_number": "You must enter a unique employee number.",
|
||||||
|
"unique_user_email": "This email is already assigned to another employee."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"esignature": {
|
"esignature": {
|
||||||
@@ -1367,6 +1368,9 @@
|
|||||||
"pdf_only": "Only PDF documents can be uploaded for e-signature.",
|
"pdf_only": "Only PDF documents can be uploaded for e-signature.",
|
||||||
"upload_title": "Unable to prepare document for e-signature"
|
"upload_title": "Unable to prepare document for e-signature"
|
||||||
},
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"contact_sales": "E-signatures are not enabled for this shop. Contact sales to add this feature."
|
||||||
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"completed": "Completed?",
|
"completed": "Completed?",
|
||||||
"completed_at": "Completed At",
|
"completed_at": "Completed At",
|
||||||
@@ -3051,6 +3055,9 @@
|
|||||||
"appointments": {
|
"appointments": {
|
||||||
"appointment_confirmation": "Appointment Confirmation"
|
"appointment_confirmation": "Appointment Confirmation"
|
||||||
},
|
},
|
||||||
|
"banners": {
|
||||||
|
"esignature_promo": "Tired of getting paper signatures? Try E-Signatures today. Contact support to add this feature."
|
||||||
|
},
|
||||||
"bills": {
|
"bills": {
|
||||||
"inhouse_invoice": "In House Invoice"
|
"inhouse_invoice": "In House Invoice"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1351,7 +1351,8 @@
|
|||||||
"vacationadded": ""
|
"vacationadded": ""
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"unique_employee_number": ""
|
"unique_employee_number": "",
|
||||||
|
"unique_user_email": "Este correo electrónico ya está asignado a otro empleado."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"esignature": {
|
"esignature": {
|
||||||
@@ -1367,6 +1368,9 @@
|
|||||||
"pdf_only": "Only PDF documents can be uploaded for e-signature.",
|
"pdf_only": "Only PDF documents can be uploaded for e-signature.",
|
||||||
"upload_title": "Unable to prepare document for e-signature"
|
"upload_title": "Unable to prepare document for e-signature"
|
||||||
},
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"contact_sales": "Las firmas electronicas no estan habilitadas para este taller. Contacte a ventas para agregar esta funcion."
|
||||||
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"completed": "",
|
"completed": "",
|
||||||
"completed_at": "",
|
"completed_at": "",
|
||||||
@@ -3051,6 +3055,9 @@
|
|||||||
"appointments": {
|
"appointments": {
|
||||||
"appointment_confirmation": ""
|
"appointment_confirmation": ""
|
||||||
},
|
},
|
||||||
|
"banners": {
|
||||||
|
"esignature_promo": "¿Cansado de obtener firmas en papel? Prueba las firmas electrónicas hoy. Contacta a ventas para agregar esta función."
|
||||||
|
},
|
||||||
"bills": {
|
"bills": {
|
||||||
"inhouse_invoice": ""
|
"inhouse_invoice": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1351,7 +1351,8 @@
|
|||||||
"vacationadded": ""
|
"vacationadded": ""
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"unique_employee_number": ""
|
"unique_employee_number": "",
|
||||||
|
"unique_user_email": "Cette adresse courriel est déjà assignée à un autre employé."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"esignature": {
|
"esignature": {
|
||||||
@@ -1367,6 +1368,9 @@
|
|||||||
"pdf_only": "Only PDF documents can be uploaded for e-signature.",
|
"pdf_only": "Only PDF documents can be uploaded for e-signature.",
|
||||||
"upload_title": "Unable to prepare document for e-signature"
|
"upload_title": "Unable to prepare document for e-signature"
|
||||||
},
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"contact_sales": "Les signatures electroniques ne sont pas activees pour cet atelier. Contactez les ventes pour ajouter cette fonctionnalite."
|
||||||
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"completed": "",
|
"completed": "",
|
||||||
"completed_at": "",
|
"completed_at": "",
|
||||||
@@ -3051,6 +3055,9 @@
|
|||||||
"appointments": {
|
"appointments": {
|
||||||
"appointment_confirmation": ""
|
"appointment_confirmation": ""
|
||||||
},
|
},
|
||||||
|
"banners": {
|
||||||
|
"esignature_promo": "Vous en avez assez des signatures papier? Essayez les signatures électroniques dès aujourd'hui. Communiquez avec les ventes pour ajouter cette fonction."
|
||||||
|
},
|
||||||
"bills": {
|
"bills": {
|
||||||
"inhouse_invoice": ""
|
"inhouse_invoice": ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -236,10 +236,8 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
redux: ["redux"],
|
redux: ["redux"],
|
||||||
lodash: ["lodash"],
|
lodash: ["lodash"],
|
||||||
"@sentry/react": ["@sentry/react"],
|
"@sentry/react": ["@sentry/react"],
|
||||||
"@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
|
"feature-flags": ["src/feature-flags/splitio-react-replacement.jsx"],
|
||||||
logrocket: ["logrocket"],
|
|
||||||
firebase: [
|
firebase: [
|
||||||
"@firebase/analytics",
|
|
||||||
"@firebase/app",
|
"@firebase/app",
|
||||||
"@firebase/firestore",
|
"@firebase/firestore",
|
||||||
"@firebase/auth",
|
"@firebase/auth",
|
||||||
|
|||||||
2
documenso/terraform/.terraform.lock.hcl
generated
2
documenso/terraform/.terraform.lock.hcl
generated
@@ -5,6 +5,7 @@ provider "registry.terraform.io/hashicorp/aws" {
|
|||||||
version = "6.38.0"
|
version = "6.38.0"
|
||||||
constraints = "~> 6.0"
|
constraints = "~> 6.0"
|
||||||
hashes = [
|
hashes = [
|
||||||
|
"h1:IMf41BcW9huOeVcrt6XjQqadYR2xD8zkUpGLLERJ4NM=",
|
||||||
"h1:RDoKIzXmt7H1mNFcNIyRT+nA/gTJyO3+iW9QGN5I2eQ=",
|
"h1:RDoKIzXmt7H1mNFcNIyRT+nA/gTJyO3+iW9QGN5I2eQ=",
|
||||||
"zh:143f118ae71059a7a7026c6b950da23fef04a06e2362ffa688bef75e43e869ed",
|
"zh:143f118ae71059a7a7026c6b950da23fef04a06e2362ffa688bef75e43e869ed",
|
||||||
"zh:29ee220a017306effd877e1280f8b2934dc957e16e0e72ca0222e5514d0db522",
|
"zh:29ee220a017306effd877e1280f8b2934dc957e16e0e72ca0222e5514d0db522",
|
||||||
@@ -28,6 +29,7 @@ provider "registry.terraform.io/hashicorp/random" {
|
|||||||
version = "3.8.1"
|
version = "3.8.1"
|
||||||
constraints = "~> 3.6"
|
constraints = "~> 3.6"
|
||||||
hashes = [
|
hashes = [
|
||||||
|
"h1:osH3aBqEARwOz3VBJKdpFKJJCNIdgRC6k8vPojkLmlY=",
|
||||||
"h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=",
|
"h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=",
|
||||||
"zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4",
|
"zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4",
|
||||||
"zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae",
|
"zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"terraform_version": "1.14.3",
|
"terraform_version": "1.15.4",
|
||||||
"serial": 105,
|
"serial": 111,
|
||||||
"lineage": "2b49a6da-17c7-01da-d62f-9a13def4b683",
|
"lineage": "2b49a6da-17c7-01da-d62f-9a13def4b683",
|
||||||
"outputs": {
|
"outputs": {
|
||||||
"application_url": {
|
"application_url": {
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"postgres_engine_version": {
|
"postgres_engine_version": {
|
||||||
"value": "17.9",
|
"value": "17.10",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"secrets_manager_secret_name": {
|
"secrets_manager_secret_name": {
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
"filter": null,
|
"filter": null,
|
||||||
"has_major_target": null,
|
"has_major_target": null,
|
||||||
"has_minor_target": null,
|
"has_minor_target": null,
|
||||||
"id": "17.9",
|
"id": "17.10",
|
||||||
"include_all": null,
|
"include_all": null,
|
||||||
"latest": true,
|
"latest": true,
|
||||||
"parameter_group_family": "postgres17",
|
"parameter_group_family": "postgres17",
|
||||||
@@ -144,15 +144,15 @@
|
|||||||
"supports_parallel_query": false,
|
"supports_parallel_query": false,
|
||||||
"supports_read_replica": true,
|
"supports_read_replica": true,
|
||||||
"valid_major_targets": [
|
"valid_major_targets": [
|
||||||
"18.3"
|
"18.4"
|
||||||
],
|
],
|
||||||
"valid_minor_targets": [],
|
"valid_minor_targets": [],
|
||||||
"valid_upgrade_targets": [
|
"valid_upgrade_targets": [
|
||||||
"18.3"
|
"18.4"
|
||||||
],
|
],
|
||||||
"version": "17.9",
|
"version": "17.10",
|
||||||
"version_actual": "17.9",
|
"version_actual": "17.10",
|
||||||
"version_description": "PostgreSQL 17.9-R1"
|
"version_description": "PostgreSQL 17.10-R1"
|
||||||
},
|
},
|
||||||
"sensitive_attributes": [],
|
"sensitive_attributes": [],
|
||||||
"identity_schema_version": 0
|
"identity_schema_version": 0
|
||||||
@@ -1085,7 +1085,7 @@
|
|||||||
"endpoint": "documenso-postgres.cfo5pnykioqq.ca-central-1.rds.amazonaws.com:5432",
|
"endpoint": "documenso-postgres.cfo5pnykioqq.ca-central-1.rds.amazonaws.com:5432",
|
||||||
"engine": "postgres",
|
"engine": "postgres",
|
||||||
"engine_lifecycle_support": "open-source-rds-extended-support",
|
"engine_lifecycle_support": "open-source-rds-extended-support",
|
||||||
"engine_version": "17.9",
|
"engine_version": "17.10",
|
||||||
"engine_version_actual": "17.9",
|
"engine_version_actual": "17.9",
|
||||||
"final_snapshot_identifier": "documenso-final-03443461",
|
"final_snapshot_identifier": "documenso-final-03443461",
|
||||||
"hosted_zone_id": "Z1JG78A3UK1DU3",
|
"hosted_zone_id": "Z1JG78A3UK1DU3",
|
||||||
@@ -1096,7 +1096,7 @@
|
|||||||
"instance_class": "db.t4g.micro",
|
"instance_class": "db.t4g.micro",
|
||||||
"iops": 3000,
|
"iops": 3000,
|
||||||
"kms_key_id": "arn:aws:kms:ca-central-1:714144183158:key/1237b672-91b3-4d23-958d-1877c5d22eb9",
|
"kms_key_id": "arn:aws:kms:ca-central-1:714144183158:key/1237b672-91b3-4d23-958d-1877c5d22eb9",
|
||||||
"latest_restorable_time": "2026-05-01T17:49:36Z",
|
"latest_restorable_time": "2026-05-25T20:16:55Z",
|
||||||
"license_model": "postgresql-license",
|
"license_model": "postgresql-license",
|
||||||
"listener_endpoint": [],
|
"listener_endpoint": [],
|
||||||
"maintenance_window": "tue:03:10-tue:03:40",
|
"maintenance_window": "tue:03:10-tue:03:40",
|
||||||
@@ -1384,7 +1384,7 @@
|
|||||||
"Application": "documenso",
|
"Application": "documenso",
|
||||||
"ManagedBy": "Terraform"
|
"ManagedBy": "Terraform"
|
||||||
},
|
},
|
||||||
"task_definition": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task:8",
|
"task_definition": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task:9",
|
||||||
"timeouts": null,
|
"timeouts": null,
|
||||||
"triggers": {},
|
"triggers": {},
|
||||||
"volume_configuration": [],
|
"volume_configuration": [],
|
||||||
@@ -1451,9 +1451,9 @@
|
|||||||
{
|
{
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"arn": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task:8",
|
"arn": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task:9",
|
||||||
"arn_without_revision": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task",
|
"arn_without_revision": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task",
|
||||||
"container_definitions": "[{\"environment\":[{\"name\":\"NEXT_PRIVATE_INTERNAL_WEBAPP_URL\",\"value\":\"http://127.0.0.1:3000\"},{\"name\":\"NEXT_PRIVATE_SMTP_HOST\",\"value\":\"email-smtp.ca-central-1.amazonaws.com\"},{\"name\":\"NEXT_PRIVATE_SMTP_PORT\",\"value\":\"587\"},{\"name\":\"NEXT_PRIVATE_SMTP_SECURE\",\"value\":\"false\"},{\"name\":\"NEXT_PRIVATE_SMTP_TRANSPORT\",\"value\":\"smtp-auth\"},{\"name\":\"NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS\",\"value\":\"false\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_BUCKET\",\"value\":\"documenso-714144183158-ca-central-1\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_REGION\",\"value\":\"ca-central-1\"},{\"name\":\"NEXT_PUBLIC_DISABLE_SIGNUP\",\"value\":\"true\"},{\"name\":\"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT\",\"value\":\"10\"},{\"name\":\"NEXT_PUBLIC_UPLOAD_TRANSPORT\",\"value\":\"s3\"},{\"name\":\"NEXT_PUBLIC_WEBAPP_URL\",\"value\":\"https://sign.imex.online\"},{\"name\":\"PORT\",\"value\":\"3000\"}],\"essential\":true,\"image\":\"documenso/documenso:2.10.0\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"/ecs/documenso\",\"awslogs-region\":\"ca-central-1\",\"awslogs-stream-prefix\":\"documenso\"}},\"mountPoints\":[],\"name\":\"documenso\",\"portMappings\":[{\"containerPort\":3000,\"hostPort\":3000,\"protocol\":\"tcp\"}],\"secrets\":[{\"name\":\"NEXTAUTH_SECRET\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXTAUTH_SECRET::\"},{\"name\":\"NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS::\"},{\"name\":\"NEXT_PRIVATE_DATABASE_URL\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DATABASE_URL::\"},{\"name\":\"NEXT_PRIVATE_DIRECT_DATABASE_URL\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DIRECT_DATABASE_URL::\"},{\"name\":\"NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY::\"},{\"name\":\"NEXT_PRIVATE_ENCRYPTION_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ENCRYPTION_KEY::\"},{\"name\":\"NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY::\"},{\"name\":\"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS::\"},{\"name\":\"NEXT_PRIVATE_SIGNING_PASSPHRASE\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SIGNING_PASSPHRASE::\"},{\"name\":\"NEXT_PRIVATE_SMTP_FROM_ADDRESS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_FROM_ADDRESS::\"},{\"name\":\"NEXT_PRIVATE_SMTP_FROM_NAME\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_FROM_NAME::\"},{\"name\":\"NEXT_PRIVATE_SMTP_PASSWORD\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_PASSWORD::\"},{\"name\":\"NEXT_PRIVATE_SMTP_USERNAME\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_USERNAME::\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID::\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY::\"}],\"systemControls\":[],\"volumesFrom\":[]}]",
|
"container_definitions": "[{\"environment\":[{\"name\":\"NEXT_PRIVATE_INTERNAL_WEBAPP_URL\",\"value\":\"http://127.0.0.1:3000\"},{\"name\":\"NEXT_PRIVATE_SMTP_HOST\",\"value\":\"email-smtp.ca-central-1.amazonaws.com\"},{\"name\":\"NEXT_PRIVATE_SMTP_PORT\",\"value\":\"587\"},{\"name\":\"NEXT_PRIVATE_SMTP_SECURE\",\"value\":\"false\"},{\"name\":\"NEXT_PRIVATE_SMTP_TRANSPORT\",\"value\":\"smtp-auth\"},{\"name\":\"NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS\",\"value\":\"false\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_BUCKET\",\"value\":\"documenso-714144183158-ca-central-1\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_REGION\",\"value\":\"ca-central-1\"},{\"name\":\"NEXT_PUBLIC_DISABLE_SIGNUP\",\"value\":\"true\"},{\"name\":\"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT\",\"value\":\"10\"},{\"name\":\"NEXT_PUBLIC_UPLOAD_TRANSPORT\",\"value\":\"s3\"},{\"name\":\"NEXT_PUBLIC_WEBAPP_URL\",\"value\":\"https://sign.imex.online\"},{\"name\":\"PORT\",\"value\":\"3000\"}],\"essential\":true,\"image\":\"documenso/documenso:2.11.0\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"/ecs/documenso\",\"awslogs-region\":\"ca-central-1\",\"awslogs-stream-prefix\":\"documenso\"}},\"mountPoints\":[],\"name\":\"documenso\",\"portMappings\":[{\"containerPort\":3000,\"hostPort\":3000,\"protocol\":\"tcp\"}],\"secrets\":[{\"name\":\"NEXTAUTH_SECRET\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXTAUTH_SECRET::\"},{\"name\":\"NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS::\"},{\"name\":\"NEXT_PRIVATE_DATABASE_URL\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DATABASE_URL::\"},{\"name\":\"NEXT_PRIVATE_DIRECT_DATABASE_URL\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DIRECT_DATABASE_URL::\"},{\"name\":\"NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY::\"},{\"name\":\"NEXT_PRIVATE_ENCRYPTION_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ENCRYPTION_KEY::\"},{\"name\":\"NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY::\"},{\"name\":\"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS::\"},{\"name\":\"NEXT_PRIVATE_SIGNING_PASSPHRASE\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SIGNING_PASSPHRASE::\"},{\"name\":\"NEXT_PRIVATE_SMTP_FROM_ADDRESS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_FROM_ADDRESS::\"},{\"name\":\"NEXT_PRIVATE_SMTP_FROM_NAME\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_FROM_NAME::\"},{\"name\":\"NEXT_PRIVATE_SMTP_PASSWORD\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_PASSWORD::\"},{\"name\":\"NEXT_PRIVATE_SMTP_USERNAME\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_USERNAME::\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID::\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY::\"}],\"systemControls\":[],\"volumesFrom\":[]}]",
|
||||||
"cpu": "512",
|
"cpu": "512",
|
||||||
"enable_fault_injection": false,
|
"enable_fault_injection": false,
|
||||||
"ephemeral_storage": [],
|
"ephemeral_storage": [],
|
||||||
@@ -1470,7 +1470,7 @@
|
|||||||
"requires_compatibilities": [
|
"requires_compatibilities": [
|
||||||
"FARGATE"
|
"FARGATE"
|
||||||
],
|
],
|
||||||
"revision": 8,
|
"revision": 9,
|
||||||
"runtime_platform": [],
|
"runtime_platform": [],
|
||||||
"skip_destroy": false,
|
"skip_destroy": false,
|
||||||
"tags": {
|
"tags": {
|
||||||
@@ -1498,7 +1498,7 @@
|
|||||||
"account_id": "714144183158",
|
"account_id": "714144183158",
|
||||||
"family": "documenso-task",
|
"family": "documenso-task",
|
||||||
"region": "ca-central-1",
|
"region": "ca-central-1",
|
||||||
"revision": 8
|
"revision": 9
|
||||||
},
|
},
|
||||||
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
|
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"terraform_version": "1.14.3",
|
"terraform_version": "1.15.4",
|
||||||
"serial": 101,
|
"serial": 105,
|
||||||
"lineage": "2b49a6da-17c7-01da-d62f-9a13def4b683",
|
"lineage": "2b49a6da-17c7-01da-d62f-9a13def4b683",
|
||||||
"outputs": {
|
"outputs": {
|
||||||
"application_url": {
|
"application_url": {
|
||||||
@@ -1096,7 +1096,7 @@
|
|||||||
"instance_class": "db.t4g.micro",
|
"instance_class": "db.t4g.micro",
|
||||||
"iops": 3000,
|
"iops": 3000,
|
||||||
"kms_key_id": "arn:aws:kms:ca-central-1:714144183158:key/1237b672-91b3-4d23-958d-1877c5d22eb9",
|
"kms_key_id": "arn:aws:kms:ca-central-1:714144183158:key/1237b672-91b3-4d23-958d-1877c5d22eb9",
|
||||||
"latest_restorable_time": "2026-05-01T15:49:30Z",
|
"latest_restorable_time": "2026-05-01T17:49:36Z",
|
||||||
"license_model": "postgresql-license",
|
"license_model": "postgresql-license",
|
||||||
"listener_endpoint": [],
|
"listener_endpoint": [],
|
||||||
"maintenance_window": "tue:03:10-tue:03:40",
|
"maintenance_window": "tue:03:10-tue:03:40",
|
||||||
@@ -3551,7 +3551,7 @@
|
|||||||
],
|
],
|
||||||
"description": "WAF protection for Documenso",
|
"description": "WAF protection for Documenso",
|
||||||
"id": "04577153-2a1a-462c-94b8-b0a1804755bb",
|
"id": "04577153-2a1a-462c-94b8-b0a1804755bb",
|
||||||
"lock_token": "e71f2816-492c-4afc-acc2-3700795c2657",
|
"lock_token": "417061f1-deea-4ac2-b932-9bea49265444",
|
||||||
"name": "documenso-web-acl",
|
"name": "documenso-web-acl",
|
||||||
"name_prefix": "",
|
"name_prefix": "",
|
||||||
"region": "ca-central-1",
|
"region": "ca-central-1",
|
||||||
@@ -3693,7 +3693,24 @@
|
|||||||
{
|
{
|
||||||
"managed_rule_group_configs": [],
|
"managed_rule_group_configs": [],
|
||||||
"name": "AWSManagedRulesCommonRuleSet",
|
"name": "AWSManagedRulesCommonRuleSet",
|
||||||
"rule_action_override": [],
|
"rule_action_override": [
|
||||||
|
{
|
||||||
|
"action_to_use": [
|
||||||
|
{
|
||||||
|
"allow": [],
|
||||||
|
"block": [],
|
||||||
|
"captcha": [],
|
||||||
|
"challenge": [],
|
||||||
|
"count": [
|
||||||
|
{
|
||||||
|
"custom_request_handling": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "SizeRestrictions_BODY"
|
||||||
|
}
|
||||||
|
],
|
||||||
"scope_down_statement": [],
|
"scope_down_statement": [],
|
||||||
"vendor_name": "AWS",
|
"vendor_name": "AWS",
|
||||||
"version": ""
|
"version": ""
|
||||||
|
|||||||
@@ -846,6 +846,13 @@
|
|||||||
table:
|
table:
|
||||||
name: exportlog
|
name: exportlog
|
||||||
schema: public
|
schema: public
|
||||||
|
- name: feature_flags
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: bodyshopid
|
||||||
|
table:
|
||||||
|
name: bodyshop_feature_flags
|
||||||
|
schema: public
|
||||||
- name: inventories
|
- name: inventories
|
||||||
using:
|
using:
|
||||||
foreign_key_constraint_on:
|
foreign_key_constraint_on:
|
||||||
@@ -2739,6 +2746,114 @@
|
|||||||
- end_date
|
- end_date
|
||||||
- content
|
- content
|
||||||
filter: {}
|
filter: {}
|
||||||
|
- table:
|
||||||
|
name: bodyshop_feature_flags
|
||||||
|
schema: public
|
||||||
|
object_relationships:
|
||||||
|
- name: bodyshop
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: bodyshopid
|
||||||
|
- name: feature_flag
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: name
|
||||||
|
select_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- id
|
||||||
|
- bodyshopid
|
||||||
|
- name
|
||||||
|
- treatment
|
||||||
|
- config
|
||||||
|
- activeDate
|
||||||
|
- deactiveDate
|
||||||
|
- created_at
|
||||||
|
- updated_at
|
||||||
|
filter:
|
||||||
|
_and:
|
||||||
|
- bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
- feature_flag:
|
||||||
|
active:
|
||||||
|
_eq: true
|
||||||
|
event_triggers:
|
||||||
|
- name: cache_bodyshop_feature_flags
|
||||||
|
definition:
|
||||||
|
delete:
|
||||||
|
columns: '*'
|
||||||
|
enable_manual: false
|
||||||
|
insert:
|
||||||
|
columns: '*'
|
||||||
|
update:
|
||||||
|
columns: '*'
|
||||||
|
retry_conf:
|
||||||
|
interval_sec: 10
|
||||||
|
num_retries: 0
|
||||||
|
timeout_sec: 60
|
||||||
|
webhook_from_env: HASURA_API_URL
|
||||||
|
headers:
|
||||||
|
- name: event-secret
|
||||||
|
value_from_env: EVENT_SECRET
|
||||||
|
request_transform:
|
||||||
|
method: POST
|
||||||
|
query_params: {}
|
||||||
|
template_engine: Kriti
|
||||||
|
url: '{{$base_url}}/feature-flags/cache/invalidate'
|
||||||
|
version: 2
|
||||||
|
- table:
|
||||||
|
name: feature_flags
|
||||||
|
schema: public
|
||||||
|
array_relationships:
|
||||||
|
- name: bodyshop_feature_flags
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on:
|
||||||
|
column: name
|
||||||
|
table:
|
||||||
|
name: bodyshop_feature_flags
|
||||||
|
schema: public
|
||||||
|
select_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- name
|
||||||
|
- description
|
||||||
|
- default_treatment
|
||||||
|
- active
|
||||||
|
- created_at
|
||||||
|
- updated_at
|
||||||
|
filter:
|
||||||
|
active:
|
||||||
|
_eq: true
|
||||||
|
event_triggers:
|
||||||
|
- name: cache_feature_flags
|
||||||
|
definition:
|
||||||
|
delete:
|
||||||
|
columns: '*'
|
||||||
|
enable_manual: false
|
||||||
|
insert:
|
||||||
|
columns: '*'
|
||||||
|
update:
|
||||||
|
columns: '*'
|
||||||
|
retry_conf:
|
||||||
|
interval_sec: 10
|
||||||
|
num_retries: 0
|
||||||
|
timeout_sec: 60
|
||||||
|
webhook_from_env: HASURA_API_URL
|
||||||
|
headers:
|
||||||
|
- name: event-secret
|
||||||
|
value_from_env: EVENT_SECRET
|
||||||
|
request_transform:
|
||||||
|
method: POST
|
||||||
|
query_params: {}
|
||||||
|
template_engine: Kriti
|
||||||
|
url: '{{$base_url}}/feature-flags/cache/invalidate'
|
||||||
|
version: 2
|
||||||
- table:
|
- table:
|
||||||
name: exportlog
|
name: exportlog
|
||||||
schema: public
|
schema: public
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS "public"."feature_flags";
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
CREATE TABLE "public"."feature_flags" (
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text NULL,
|
||||||
|
"default_treatment" text NOT NULL DEFAULT 'off',
|
||||||
|
"active" boolean NOT NULL DEFAULT true,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT "feature_flags_pkey" PRIMARY KEY ("name"),
|
||||||
|
CONSTRAINT "feature_flags_default_treatment_check" CHECK (length(btrim("default_treatment")) > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO "public"."feature_flags" ("name", "description")
|
||||||
|
VALUES
|
||||||
|
('ADPPayroll', 'Enable ADP payroll flows and reporting.'),
|
||||||
|
('Allow_Negative_Jobline_Price', 'Allow negative pricing on job lines.'),
|
||||||
|
('Autohouse_Detail_line', 'Enable Autohouse detail line handling.'),
|
||||||
|
('Bill_OCR_AI', 'Enable AI bill OCR entry.'),
|
||||||
|
('ClosingPeriod', 'Enable closing period accounting restrictions.'),
|
||||||
|
('CriticalPartsScanning', 'Enable critical parts scanning workflows.'),
|
||||||
|
('Direct_Media_Download', 'Enable direct media downloads.'),
|
||||||
|
('DmsAp', 'Enable DMS accounts payable workflows.'),
|
||||||
|
('Enhanced_Payroll', 'Enable enhanced payroll and labor allocation features.'),
|
||||||
|
('Extended_Bill_Posting', 'Enable extended bill posting.'),
|
||||||
|
('Fortellis', 'Enable Fortellis-backed DMS flows.'),
|
||||||
|
('IOU_Tracking', 'Enable IOU tracking.'),
|
||||||
|
('ImEXPay', 'Enable ImEX Pay workflows.'),
|
||||||
|
('Imgproxy', 'Enable imgproxy-backed media rendering.'),
|
||||||
|
('LogRocket_Tracking', 'Enable LogRocket tracking.'),
|
||||||
|
('NewPhotoViewer', 'Enable the newer photo viewer experience.'),
|
||||||
|
('OEConnection', 'Enable OEConnection parts ordering.'),
|
||||||
|
('OEConnection_PriceChange', 'Enable OEConnection price changes.'),
|
||||||
|
('OpenSearch', 'Enable OpenSearch global search.'),
|
||||||
|
('OpenSearch_PaginatedScreens', 'Enable OpenSearch on paginated screens.'),
|
||||||
|
('Production_List_Status_Colors', 'Enable status colors on production list.'),
|
||||||
|
('Production_Use_View', 'Enable production view selection.'),
|
||||||
|
('Qb_Multi_Ar', 'Enable QuickBooks multi-AR payment options.'),
|
||||||
|
('Realtime_Notifications_UI', 'Enable realtime notification UI.'),
|
||||||
|
('Share_To_Teams', 'Enable sharing workflows to Microsoft Teams.'),
|
||||||
|
('Simple_Inventory', 'Enable simple inventory workflows.'),
|
||||||
|
('TEST_FLAG', 'Manual test flag used to verify frontend feature flag plumbing.'),
|
||||||
|
('Use_Graphql_RR', 'Enable GraphQL-backed Rome/RR flows.'),
|
||||||
|
('Websocket_Production', 'Toggle websocket production board/list behavior.')
|
||||||
|
ON CONFLICT ("name") DO NOTHING;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
DROP TRIGGER IF EXISTS "set_public_feature_flags_updated_at" ON "public"."feature_flags";
|
||||||
|
DROP TRIGGER IF EXISTS "set_public_bodyshop_feature_flags_updated_at" ON "public"."bodyshop_feature_flags";
|
||||||
|
DROP TABLE IF EXISTS "public"."bodyshop_feature_flags";
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
CREATE TABLE "public"."bodyshop_feature_flags" (
|
||||||
|
"id" uuid NOT NULL DEFAULT public.gen_random_uuid(),
|
||||||
|
"bodyshopid" uuid NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"treatment" text NOT NULL DEFAULT 'off',
|
||||||
|
"config" jsonb NULL,
|
||||||
|
"activeDate" timestamptz NULL,
|
||||||
|
"deactiveDate" timestamptz NULL,
|
||||||
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT "bodyshop_feature_flags_pkey" PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "bodyshop_feature_flags_bodyshopid_name_key" UNIQUE ("bodyshopid", "name"),
|
||||||
|
CONSTRAINT "bodyshop_feature_flags_bodyshopid_fkey" FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "bodyshop_feature_flags_name_fkey" FOREIGN KEY ("name") REFERENCES "public"."feature_flags" ("name") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "bodyshop_feature_flags_treatment_check" CHECK (length(btrim("treatment")) > 0),
|
||||||
|
CONSTRAINT "bodyshop_feature_flags_dates_check" CHECK ("deactiveDate" IS NULL OR "activeDate" IS NULL OR "deactiveDate" > "activeDate")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "bodyshop_feature_flags_bodyshopid_idx" ON "public"."bodyshop_feature_flags" ("bodyshopid");
|
||||||
|
CREATE INDEX "bodyshop_feature_flags_name_idx" ON "public"."bodyshop_feature_flags" ("name");
|
||||||
|
|
||||||
|
INSERT INTO "public"."bodyshop_feature_flags" (
|
||||||
|
"bodyshopid",
|
||||||
|
"name",
|
||||||
|
"treatment",
|
||||||
|
"config",
|
||||||
|
"activeDate",
|
||||||
|
"deactiveDate"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
"bodyshops"."id",
|
||||||
|
"feature_flag"."name",
|
||||||
|
CASE
|
||||||
|
WHEN jsonb_typeof("feature_flag"."value") = 'object'
|
||||||
|
AND nullif(btrim("feature_flag"."value" ->> 'treatment'), '') IS NOT NULL
|
||||||
|
THEN btrim("feature_flag"."value" ->> 'treatment')
|
||||||
|
WHEN jsonb_typeof("feature_flag"."value") = 'boolean'
|
||||||
|
THEN CASE WHEN ("feature_flag"."value" #>> '{}')::boolean THEN 'on' ELSE 'off' END
|
||||||
|
WHEN jsonb_typeof("feature_flag"."value") = 'string'
|
||||||
|
AND nullif(btrim("feature_flag"."value" #>> '{}'), '') IS NOT NULL
|
||||||
|
THEN btrim("feature_flag"."value" #>> '{}')
|
||||||
|
ELSE 'on'
|
||||||
|
END,
|
||||||
|
CASE
|
||||||
|
WHEN jsonb_typeof("feature_flag"."value") = 'object'
|
||||||
|
THEN "feature_flag"."value" -> 'config'
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
CASE
|
||||||
|
WHEN jsonb_typeof("feature_flag"."value") = 'object'
|
||||||
|
AND "feature_flag"."value" ->> 'activeDate' ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}'
|
||||||
|
THEN ("feature_flag"."value" ->> 'activeDate')::timestamptz
|
||||||
|
ELSE NULL
|
||||||
|
END,
|
||||||
|
CASE
|
||||||
|
WHEN jsonb_typeof("feature_flag"."value") = 'object'
|
||||||
|
AND "feature_flag"."value" ->> 'deactiveDate' ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}'
|
||||||
|
THEN ("feature_flag"."value" ->> 'deactiveDate')::timestamptz
|
||||||
|
ELSE NULL
|
||||||
|
END
|
||||||
|
FROM "public"."bodyshops"
|
||||||
|
CROSS JOIN LATERAL jsonb_each(
|
||||||
|
CASE
|
||||||
|
WHEN jsonb_typeof(COALESCE("bodyshops"."features" -> 'featureFlags', '{}'::jsonb)) = 'object'
|
||||||
|
THEN COALESCE("bodyshops"."features" -> 'featureFlags', '{}'::jsonb)
|
||||||
|
ELSE '{}'::jsonb
|
||||||
|
END
|
||||||
|
) AS "feature_flag"("name", "value")
|
||||||
|
INNER JOIN "public"."feature_flags" ON "feature_flags"."name" = "feature_flag"."name"
|
||||||
|
ON CONFLICT ("bodyshopid", "name") DO UPDATE
|
||||||
|
SET
|
||||||
|
"treatment" = EXCLUDED."treatment",
|
||||||
|
"config" = EXCLUDED."config",
|
||||||
|
"activeDate" = EXCLUDED."activeDate",
|
||||||
|
"deactiveDate" = EXCLUDED."deactiveDate";
|
||||||
|
|
||||||
|
CREATE TRIGGER "set_public_bodyshop_feature_flags_updated_at"
|
||||||
|
BEFORE UPDATE ON "public"."bodyshop_feature_flags"
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION "public"."set_current_timestamp_updated_at"();
|
||||||
|
|
||||||
|
COMMENT ON TRIGGER "set_public_bodyshop_feature_flags_updated_at" ON "public"."bodyshop_feature_flags"
|
||||||
|
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
|
||||||
|
|
||||||
|
CREATE TRIGGER "set_public_feature_flags_updated_at"
|
||||||
|
BEFORE UPDATE ON "public"."feature_flags"
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION "public"."set_current_timestamp_updated_at"();
|
||||||
|
|
||||||
|
COMMENT ON TRIGGER "set_public_feature_flags_updated_at" ON "public"."feature_flags"
|
||||||
|
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
ALTER TABLE "public"."feature_flags"
|
||||||
|
DROP CONSTRAINT IF EXISTS "feature_flags_default_treatment_check";
|
||||||
|
|
||||||
|
ALTER TABLE "public"."feature_flags"
|
||||||
|
ADD CONSTRAINT "feature_flags_default_treatment_check" CHECK ("default_treatment" IN ('on', 'off', 'control'));
|
||||||
|
|
||||||
|
ALTER TABLE "public"."bodyshop_feature_flags"
|
||||||
|
DROP CONSTRAINT IF EXISTS "bodyshop_feature_flags_treatment_check";
|
||||||
|
|
||||||
|
ALTER TABLE "public"."bodyshop_feature_flags"
|
||||||
|
ADD CONSTRAINT "bodyshop_feature_flags_treatment_check" CHECK ("treatment" IN ('on', 'off', 'control'));
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
ALTER TABLE "public"."feature_flags"
|
||||||
|
DROP CONSTRAINT IF EXISTS "feature_flags_default_treatment_check";
|
||||||
|
|
||||||
|
ALTER TABLE "public"."feature_flags"
|
||||||
|
ADD CONSTRAINT "feature_flags_default_treatment_check" CHECK (length(btrim("default_treatment")) > 0);
|
||||||
|
|
||||||
|
ALTER TABLE "public"."bodyshop_feature_flags"
|
||||||
|
DROP CONSTRAINT IF EXISTS "bodyshop_feature_flags_treatment_check";
|
||||||
|
|
||||||
|
ALTER TABLE "public"."bodyshop_feature_flags"
|
||||||
|
ADD CONSTRAINT "bodyshop_feature_flags_treatment_check" CHECK (length(btrim("treatment")) > 0);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DELETE FROM "public"."feature_flags"
|
||||||
|
WHERE "name" = 'TEST_FLAG';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
INSERT INTO "public"."feature_flags" ("name", "description")
|
||||||
|
VALUES ('TEST_FLAG', 'Manual test flag used to verify frontend feature flag plumbing.')
|
||||||
|
ON CONFLICT ("name") DO NOTHING;
|
||||||
2166
package-lock.json
generated
2166
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
61
package.json
61
package.json
@@ -15,66 +15,67 @@
|
|||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"test:unit": "vitest run",
|
"test:unit": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js",
|
||||||
|
"feature-flags:export-harness": "node scripts/export-harness-feature-flags.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.1020.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.1053.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.1020.0",
|
"@aws-sdk/client-elasticache": "^3.1053.0",
|
||||||
"@aws-sdk/client-s3": "^3.1020.0",
|
"@aws-sdk/client-s3": "^3.1053.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.1020.0",
|
"@aws-sdk/client-secrets-manager": "^3.1053.0",
|
||||||
"@aws-sdk/client-ses": "^3.1020.0",
|
"@aws-sdk/client-ses": "^3.1053.0",
|
||||||
"@aws-sdk/client-sqs": "^3.1020.0",
|
"@aws-sdk/client-sqs": "^3.1053.0",
|
||||||
"@aws-sdk/client-textract": "^3.1020.0",
|
"@aws-sdk/client-textract": "^3.1053.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.972.28",
|
"@aws-sdk/credential-provider-node": "^3.972.44",
|
||||||
"@aws-sdk/lib-storage": "^3.1020.0",
|
"@aws-sdk/lib-storage": "^3.1053.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.1020.0",
|
"@aws-sdk/s3-request-presigner": "^3.1053.0",
|
||||||
"@documenso/sdk-typescript": "^0.8.0",
|
"@documenso/sdk-typescript": "^0.8.1",
|
||||||
"@jsreport/nodejs-client": "^4.1.0",
|
"@jsreport/nodejs-client": "^4.1.1",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"aws4": "^1.13.2",
|
"aws4": "^1.13.2",
|
||||||
"axios": "^1.14.0",
|
"axios": "^1.16.1",
|
||||||
"axios-curlirize": "^2.0.0",
|
"axios-curlirize": "^2.0.0",
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bullmq": "^5.71.1",
|
"bullmq": "^5.77.3",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"cloudinary": "^2.9.0",
|
"cloudinary": "^2.10.0",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"crisp-status-reporter": "^1.2.2",
|
"crisp-status-reporter": "^1.2.2",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"fast-xml-parser": "^5.5.9",
|
"fast-xml-parser": "^5.8.0",
|
||||||
"firebase-admin": "^13.7.0",
|
"firebase-admin": "^13.10.0",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.3.0",
|
||||||
"graphql": "^16.13.2",
|
"graphql": "^16.14.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"intuit-oauth": "^4.2.2",
|
"intuit-oauth": "^4.2.3",
|
||||||
"ioredis": "^5.10.1",
|
"ioredis": "^5.10.1",
|
||||||
"json-2-csv": "^5.5.10",
|
"json-2-csv": "^5.5.10",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"juice": "^11.1.1",
|
"juice": "^11.1.1",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.18.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"moment-timezone": "^0.6.1",
|
"moment-timezone": "^0.6.2",
|
||||||
"multer": "^2.1.1",
|
"multer": "^2.1.1",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"node-persist": "^4.0.4",
|
"node-persist": "^4.0.4",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
"normalize-url": "^9.0.0",
|
"normalize-url": "^9.0.1",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"phone": "^3.1.71",
|
"phone": "^3.1.71",
|
||||||
"query-string": "7.1.3",
|
"query-string": "7.1.3",
|
||||||
"recursive-diff": "^1.0.9",
|
"recursive-diff": "^1.0.9",
|
||||||
"rimraf": "^6.1.3",
|
"rimraf": "^6.1.3",
|
||||||
"skia-canvas": "^3.0.8",
|
"skia-canvas": "^3.0.8",
|
||||||
"soap": "^1.8.0",
|
"soap": "^1.9.3",
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"socket.io-adapter": "^2.5.6",
|
"socket.io-adapter": "^2.5.7",
|
||||||
"ssh2-sftp-client": "^11.0.0",
|
"ssh2-sftp-client": "^11.0.0",
|
||||||
"twilio": "^5.13.1",
|
"twilio": "^5.13.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
@@ -89,11 +90,11 @@
|
|||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.39.2",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.6.0",
|
||||||
"mock-require": "^3.0.3",
|
"mock-require": "^3.0.3",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.3",
|
||||||
"supertest": "^7.2.2",
|
"supertest": "^7.2.2",
|
||||||
"vitest": "^4.1.2"
|
"vitest": "^4.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user