- |
+ |
|
-
+ |
{canUpdateDeleteConstant && (
- |
+ |
{
/>
)}
-
-
- {capitalize(activeTabEnvironment?.name)} ({globalCount + secretCount})
-
-
- {canCreateVariable() && (
- {
- setMode(() => MODES.CREATE);
- setIsManageVarDrawerOpen(() => true);
- }}
- className="add-new-constant-button"
- customStyles={{ minWidth: '200px', height: '32px' }}
- disabled={isManageVarDrawerOpen}
- >
- + Create new constant
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {capitalize(activeTabEnvironment?.name)} ({globalCount + secretCount})
+
+
+ {canCreateVariable() && (
+ {
+ setMode(() => MODES.CREATE);
+ setIsManageVarDrawerOpen(() => true);
+ }}
+ className="add-new-constant-button"
+ customStyles={{ minWidth: '200px', height: '32px' }}
+ disabled={isManageVarDrawerOpen}
+ >
+ + Create new constant
+
+ )}
-
-
-
-
-
-
-
-
- {activeTab === Constants.Global ? (
- <>
- To resolve a global workspace constant use{' '}
- {'{{constants.access_token}}'}
- >
- ) : (
- <>
- To resolve a secret workspace constant use{' '}
- {'{{secrets.access_token}}'}
- >
- )}
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
- {(activeTab === Constants.Global && globalCount > 0) ||
- (activeTab === Constants.Secret && secretCount > 0) ? (
-
-
- 0}
- />
-
- ) : (
-
+
- )}
+
+
+
+
+
+
+
+
+
+ {activeTab === Constants.Global ? (
+ <>
+ To resolve a global workspace constant use{' '}
+
+ {'{{constants.access_token}}'}
+
+ >
+ ) : (
+ <>
+ To resolve a secret workspace constant use{' '}
+ {'{{secrets.access_token}}'}
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {(activeTab === Constants.Global && globalCount > 0) ||
+ (activeTab === Constants.Secret && secretCount > 0) ? (
+
+
+ 0}
+ />
+
+ ) : (
+
+ )}
+
diff --git a/frontend/src/_components/DarkModeToggle.jsx b/frontend/src/_components/DarkModeToggle.jsx
index 2f0ef8474c..c492117dd7 100644
--- a/frontend/src/_components/DarkModeToggle.jsx
+++ b/frontend/src/_components/DarkModeToggle.jsx
@@ -4,6 +4,8 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Tooltip from 'react-bootstrap/Tooltip';
import { useTranslation } from 'react-i18next';
import classnames from 'classnames';
+import useStore from '@/AppBuilder/_stores/store';
+import { shallow } from 'zustand/shallow';
export const DarkModeToggle = function DarkModeToggle({
darkMode = false,
@@ -11,8 +13,14 @@ export const DarkModeToggle = function DarkModeToggle({
tooltipPlacement = 'bottom',
showText = false,
}) {
+ const setResolvedGlobals = useStore((state) => state.setResolvedGlobals, shallow);
+ const appMode = useStore((state) => state.globalSettings.appMode, shallow);
+
const toggleDarkMode = () => {
switchDarkMode(!darkMode);
+ if (appMode === 'auto') {
+ setResolvedGlobals('theme', { name: !darkMode ? 'dark' : 'light' });
+ }
};
const { t } = useTranslation();
diff --git a/frontend/src/_components/NotificationBanner/NotificationBanner.jsx b/frontend/src/_components/NotificationBanner/NotificationBanner.jsx
new file mode 100644
index 0000000000..8b61b37f1d
--- /dev/null
+++ b/frontend/src/_components/NotificationBanner/NotificationBanner.jsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { Alert } from '@/_ui/Alert/Alert';
+import './resources/styles.scss';
+
+const DEFAULT_CONFIG = {
+ docsLink: ' https://docs.tooljet.com/docs/data-sources/local-data-sources-migration',
+};
+
+const DEFAULT_MESSAGES = {
+ prefix: 'This query is connected to a local data source which has been',
+ highlightedText: 'discontinued',
+ middle: 'Please create a global data source connection to reconnect your query.',
+ suffix: 'to know more.',
+ linkText: 'Read documentation',
+};
+
+const NotificationBanner = ({
+ docsLink,
+ customMessage,
+ darkMode = false,
+ highlightedText = DEFAULT_MESSAGES.highlightedText,
+ highlightedClassName = 'highlighted-text',
+ enhanceDisabledVisibility = false,
+}) => {
+ const currentDocsLink = docsLink || DEFAULT_CONFIG.docsLink;
+
+ const bannerMessage = customMessage || (
+ <>
+ {DEFAULT_MESSAGES.prefix} {highlightedText}.{' '}
+ {DEFAULT_MESSAGES.middle}{' '}
+
+ {DEFAULT_MESSAGES.linkText}
+ {' '}
+ {DEFAULT_MESSAGES.suffix}
+ >
+ );
+
+ return (
+
+ );
+};
+
+export default NotificationBanner;
+
+// To Do later: Expand this component properly to make it generic notification component
diff --git a/frontend/src/_components/NotificationBanner/index.js b/frontend/src/_components/NotificationBanner/index.js
new file mode 100644
index 0000000000..e762ccced6
--- /dev/null
+++ b/frontend/src/_components/NotificationBanner/index.js
@@ -0,0 +1 @@
+export { default } from './NotificationBanner';
diff --git a/frontend/src/_components/NotificationBanner/resources/styles.scss b/frontend/src/_components/NotificationBanner/resources/styles.scss
new file mode 100644
index 0000000000..7c8d644933
--- /dev/null
+++ b/frontend/src/_components/NotificationBanner/resources/styles.scss
@@ -0,0 +1,55 @@
+.notification-banner {
+ display: flex;
+ padding: var(--3, 6px) var(--6, 12px);
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ gap: 32px;
+ flex: 1 0 0;
+ border-radius: var(--3, 6px);
+ background: var(--background-warning-weak, #FAEFE7);
+ border: none !important;
+ outline: none !important;
+ opacity: 1;
+
+ img {
+ margin-top: -5px !important;
+ }
+}
+
+@media screen and (min-width: 1200px) {
+ .notification-banner {
+ justify-content: center !important;
+ align-items: center !important;
+ }
+
+ &>div {
+ justify-content: center !important;
+ }
+
+}
+
+.notification-content {
+ color: var(--text-default, #1B1F24);
+ font-family: "IBM Plex Sans";
+ font-size: 11px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 16px;
+
+ &.disabled {
+ font-weight: 700 !important;
+ opacity: 1 !important;
+ }
+}
+
+.highlighted-text {
+ font-weight: 600 !important;
+ font-style: bold;
+}
+
+.documentation-link {
+ font-weight: 400 !important;
+ color: var(--primary, #3E63DD) !important;
+ text-decoration-line: underline !important;
+}
\ No newline at end of file
diff --git a/frontend/src/_components/OrganizationManager/CreateOrganization.jsx b/frontend/src/_components/OrganizationManager/CreateOrganization.jsx
index 82bd046ae1..ef256014dd 100644
--- a/frontend/src/_components/OrganizationManager/CreateOrganization.jsx
+++ b/frontend/src/_components/OrganizationManager/CreateOrganization.jsx
@@ -148,9 +148,9 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
setSlug({ value: defaultValue, error: '' });
const checkWorkspaceUniqueness = async () => {
+ sluginput.current.value = defaultValue;
try {
await organizationService.checkWorkspaceUniqueness(null, defaultValue);
- sluginput.current.value = defaultValue;
} catch (errResponse) {
let error = {
status: false,
diff --git a/frontend/src/_components/SearchBox.jsx b/frontend/src/_components/SearchBox.jsx
index 1e910dd519..f0c09054dc 100644
--- a/frontend/src/_components/SearchBox.jsx
+++ b/frontend/src/_components/SearchBox.jsx
@@ -21,6 +21,7 @@ export const SearchBox = forwardRef(
autoFocus = false,
showClearButton,
initialValue = '',
+ clearTextOnBlur = true,
},
ref
) => {
@@ -39,7 +40,7 @@ export const SearchBox = forwardRef(
};
const handleClickOutside = (event) => {
- if (ref?.current && !ref.current.contains(event.target)) {
+ if (ref?.current && !ref.current.contains(event.target) && clearTextOnBlur) {
clearSearchText();
// Your function to be triggered
}
diff --git a/frontend/src/_components/SortableList/SortableList.jsx b/frontend/src/_components/SortableList/SortableList.jsx
index 7a59e7fda9..4825aabfe1 100644
--- a/frontend/src/_components/SortableList/SortableList.jsx
+++ b/frontend/src/_components/SortableList/SortableList.jsx
@@ -10,10 +10,10 @@ export function SortableList({ items, onChange, renderItem }) {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { delay: 150 },
- }),
- useSensor(KeyboardSensor, {
- coordinateGetter: sortableKeyboardCoordinates,
})
+ // useSensor(KeyboardSensor, {
+ // coordinateGetter: sortableKeyboardCoordinates,
+ // })
);
const shouldFreeze = useStore((state) => state.isVersionReleased || state.isEditorFreezed);
diff --git a/frontend/src/_styles/components.scss b/frontend/src/_styles/components.scss
index 9399081d0f..d7c10868ea 100644
--- a/frontend/src/_styles/components.scss
+++ b/frontend/src/_styles/components.scss
@@ -123,7 +123,6 @@ $btn-dark-color: #FFFFFF;
.page-selector-panel-body {
- height: 100%;
padding: 12px 16px;
border-right: 1px solid #DFE3E6;
diff --git a/frontend/src/_styles/pages-sidebar.scss b/frontend/src/_styles/pages-sidebar.scss
index 9c9e0616ed..c1ad5a7111 100644
--- a/frontend/src/_styles/pages-sidebar.scss
+++ b/frontend/src/_styles/pages-sidebar.scss
@@ -12,7 +12,7 @@
}
.page-name {
- font-size: 14px;
+ font-size: 12px;
}
.navigation-area {
diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss
index a5987eee1d..07895c1392 100644
--- a/frontend/src/_styles/theme.scss
+++ b/frontend/src/_styles/theme.scss
@@ -766,7 +766,7 @@ button {
right: 300px;
left: 48px;
overflow-y: scroll;
- overflow-x: auto;
+ overflow-x: hidden;
-webkit-box-pack: center;
justify-content: center;
-webkit-box-align: center;
@@ -3875,6 +3875,18 @@ input[type="text"] {
}
}
+.form-ele{
+ .DateRangePicker_picker{
+ top: 40px !important;
+ }
+
+ .daterange-picker-widget{
+ .DateInput_fang {
+ visibility: hidden !important;
+ }
+ }
+}
+
.fw-400 {
font-weight: 400;
}
@@ -7852,7 +7864,7 @@ tbody {
.marketplace-page-sidebar {
height: calc(100vh - 64px);
- max-width: 288px;
+ max-width: 272px;
background-color: var(--page-default);
border-right: 1px solid var(--slate5) !important;
display: grid !important;
@@ -9195,6 +9207,17 @@ tbody {
.spin-loader {
position: fixed;
width: 100%;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+
+ &.theme-dark {
+ background-color: #1f2936;
+
+ .load {
+ background-color: #1f2936;
+ }
+ }
.load {
display: flex;
@@ -12701,7 +12724,7 @@ color: var(--text-default);
position: absolute;
display: flex;
justify-content: center;
- top: 55px;
+ top: 65px;
.released-version-popup-cover {
width: 250px;
@@ -12807,6 +12830,11 @@ color: var(--text-default);
padding: 16px;
padding-top: 0px;
padding-bottom: 0px;
+
+ .p-3-constants{
+ padding: 1rem !important;
+ padding-left: 0px !important;
+ }
}
.card-footer {
@@ -15546,9 +15574,10 @@ color: var(--text-default);
background-color: var(--page-default);
height: calc(100vh - 64px);
display: flex;
- align-items: center;
- justify-content: center;
- padding-top: 1.5rem;
+ //Uncomment for EE
+ // align-items: center;
+ // justify-content: center;
+ // padding-top: 1.5rem;
}
.blank-page-wrapper {
diff --git a/frontend/src/_ui/AppButton/AppButton.scss b/frontend/src/_ui/AppButton/AppButton.scss
index 15320ad44e..fd39d9e9c3 100644
--- a/frontend/src/_ui/AppButton/AppButton.scss
+++ b/frontend/src/_ui/AppButton/AppButton.scss
@@ -21,6 +21,16 @@
align-items: center;
}
+ &.table-pagination-btn{
+ &:disabled {
+ svg {
+ path {
+ fill: var(--slate8) !important;
+ }
+ }
+ }
+ }
+
&:disabled {
background: var(--slate6) !important;
color: var(--slate9) !important;
diff --git a/frontend/src/index.ejs b/frontend/src/index.ejs
index ba87ce339c..e38f611797 100644
--- a/frontend/src/index.ejs
+++ b/frontend/src/index.ejs
@@ -25,6 +25,7 @@
justify-content: center;
align-items: center;
height: 100vh;
+ background-color: white;
}
.load.dark-loader {
diff --git a/frontend/src/modules/common/components/GeneralFeatureImage/resources/images/feat-image-dark.svg b/frontend/src/modules/common/components/GeneralFeatureImage/resources/images/feat-image-dark.svg
index 083675fe33..a52bc469dc 100644
--- a/frontend/src/modules/common/components/GeneralFeatureImage/resources/images/feat-image-dark.svg
+++ b/frontend/src/modules/common/components/GeneralFeatureImage/resources/images/feat-image-dark.svg
@@ -1,9 +1,9 @@
-
\ No newline at end of file
diff --git a/frontend/src/modules/common/components/GeneralFeatureImage/resources/images/feat-image.svg b/frontend/src/modules/common/components/GeneralFeatureImage/resources/images/feat-image.svg
index 9acb1cd58d..4e4b16c5ad 100644
--- a/frontend/src/modules/common/components/GeneralFeatureImage/resources/images/feat-image.svg
+++ b/frontend/src/modules/common/components/GeneralFeatureImage/resources/images/feat-image.svg
@@ -1,9 +1,9 @@
-
-
+
+
-
-
+
+
-
+
-
+
\ No newline at end of file
diff --git a/frontend/src/modules/common/components/GeneralFeatureImage/resources/images/slogan.svg b/frontend/src/modules/common/components/GeneralFeatureImage/resources/images/slogan.svg
index 580b2629e8..f969d93b1a 100644
--- a/frontend/src/modules/common/components/GeneralFeatureImage/resources/images/slogan.svg
+++ b/frontend/src/modules/common/components/GeneralFeatureImage/resources/images/slogan.svg
@@ -1,7 +1,7 @@
-
-
+
+
-
+
diff --git a/frontend/src/modules/onboarding/components/OnboardingQuestions/components/WorkspaceNameFormCE/WorkspaceNameFormCE.jsx b/frontend/src/modules/onboarding/components/OnboardingQuestions/components/WorkspaceNameFormCE/WorkspaceNameFormCE.jsx
index a1e69cbe53..559111e8e5 100644
--- a/frontend/src/modules/onboarding/components/OnboardingQuestions/components/WorkspaceNameFormCE/WorkspaceNameFormCE.jsx
+++ b/frontend/src/modules/onboarding/components/OnboardingQuestions/components/WorkspaceNameFormCE/WorkspaceNameFormCE.jsx
@@ -47,7 +47,12 @@ const WorkspaceNameFormCE = () => {
shallow
);
useEnterKeyPress(() => handleSubmit());
-
+ const { setOnboardingStepsCompleted } = useOnboardingStore(
+ (state) => ({
+ setOnboardingStepsCompleted: state.setOnboardingStepsCompleted,
+ }),
+ shallow
+ );
const [formData, setFormData] = useState({ workspaceName: workspaceName });
const [error, setError] = useState('');
const [isFormValid, setIsFormValid] = useState(true);
@@ -121,6 +126,7 @@ const WorkspaceNameFormCE = () => {
try {
await setWorkspaceName(formData.workspaceName);
await onboardUserOrCreateAdmin();
+ setOnboardingStepsCompleted();
} catch (error) {
const errorMessage = error?.error || 'Something went wrong. Please try again.';
toast.error(errorMessage);
diff --git a/frontend/src/modules/onboarding/components/OnboardingUIWrapper/OnboardingUIWrapper.jsx b/frontend/src/modules/onboarding/components/OnboardingUIWrapper/OnboardingUIWrapper.jsx
index 15a783c6df..81b84569eb 100644
--- a/frontend/src/modules/onboarding/components/OnboardingUIWrapper/OnboardingUIWrapper.jsx
+++ b/frontend/src/modules/onboarding/components/OnboardingUIWrapper/OnboardingUIWrapper.jsx
@@ -3,8 +3,12 @@ import OnboardingFormWrapper from '../OnboardingFormWrapper/OnboardingFormWrappe
import './resources/styles/onboarding-ui-wrapper.styles.scss';
const OnboardingUIWrapper = ({ children: components }) => {
- const pageLocation = window.location.pathname;
- if (pageLocation == '/setup') {
+ const isEmptyPath = window.location.pathname == '/';
+ const isSetupRoute = window.location.pathname.split('/').pop().toLowerCase() === 'setup';
+ const pathEndSegments = window.location.pathname.split('/').filter(Boolean).slice(-2);
+ const isInvitationRoute =
+ pathEndSegments.length === 2 && pathEndSegments[0] === 'invitations' && pathEndSegments[1]?.length > 0;
+ if (isSetupRoute || isInvitationRoute || isEmptyPath) {
return (
{components}
diff --git a/frontend/src/modules/onboarding/pages/InvitationPage/InvitationPage.jsx b/frontend/src/modules/onboarding/pages/InvitationPage/InvitationPage.jsx
index 948736b944..1288828e7a 100644
--- a/frontend/src/modules/onboarding/pages/InvitationPage/InvitationPage.jsx
+++ b/frontend/src/modules/onboarding/pages/InvitationPage/InvitationPage.jsx
@@ -7,8 +7,9 @@ import invitationsStore from '@/modules/onboarding/stores/invitationsStore';
import { LinkExpiredPage } from '@/ConfirmationPage/LinkExpiredPage';
import { utils } from '@/modules/common/helpers';
import { getSubpath } from '@/_helpers/routes';
-
-const PostOnboardingComponent = () => null;
+import { TJLoader } from '@/_ui/TJLoader/TJLoader';
+import useOnboardingStore from '@/modules/onboarding/stores/onboardingStore';
+const PostOnboardingComponent = () => ;
export const InvitationPage = (darkMode = false) => {
const [isLoading, setIsLoading] = useState(true);
@@ -23,8 +24,8 @@ export const InvitationPage = (darkMode = false) => {
const source = searchParams.get('source');
const redirectTo = searchParams.get('redirectTo');
- const { initiateInvitedUserOnboarding, isOnboardingStepsCompleted } = invitationsStore();
-
+ const { initiateInvitedUserOnboarding } = invitationsStore();
+ const { isOnboardingStepsCompleted } = useOnboardingStore();
useEffect(() => {
getUserDetails();
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/frontend/src/modules/onboarding/pages/SetupAdminPage/SetupAdminPageCE.jsx b/frontend/src/modules/onboarding/pages/SetupAdminPage/SetupAdminPageCE.jsx
index ffd6afeaca..5f7aa4c3e9 100644
--- a/frontend/src/modules/onboarding/pages/SetupAdminPage/SetupAdminPageCE.jsx
+++ b/frontend/src/modules/onboarding/pages/SetupAdminPage/SetupAdminPageCE.jsx
@@ -1,19 +1,23 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import { OnboardingBackgroundWrapper, OnboardingQuestions } from '@/modules/onboarding/components';
import { SetupAdminForm } from './components';
import { GeneralFeatureImage } from '@/modules/common/components';
import useOnboardingStore from '@/modules/onboarding/stores/onboardingStore';
import { shallow } from 'zustand/shallow';
+import { TJLoader } from '@/_ui/TJLoader/TJLoader';
+const PostOnboardingComponent = () => ;
const SetupAdminPageCE = () => {
- const { currentStep } = useOnboardingStore(
+ const { currentStep, isOnboardingStepsCompleted } = useOnboardingStore(
(state) => ({
currentStep: state.currentStep,
+ isOnboardingStepsCompleted: state.isOnboardingStepsCompleted,
}),
shallow
);
-
- if (currentStep > 0) {
+ if (isOnboardingStepsCompleted && PostOnboardingComponent) {
+ return ;
+ } else if (currentStep > 0) {
return ;
}
diff --git a/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx b/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx
index d411d7018e..fa0b4038f7 100644
--- a/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx
+++ b/frontend/src/modules/onboarding/pages/SignupPage/SignupPage.jsx
@@ -38,13 +38,15 @@ const SignupPage = ({ configs, organizationId }) => {
});
}, []);
- const handleSignup = (formData, onSuccess = () => {}, onFaluire = () => {}) => {
+ const handleSignup = (formData, onSuccess = () => {}, onFailure = () => {}) => {
const { email, name, password } = formData;
-
if (organizationToken) {
authenticationService
.activateAccountWithToken(email, password, organizationToken)
- .then((response) => onInvitedUserSignUpSuccess(response, navigate))
+ .then((response) => {
+ onInvitedUserSignUpSuccess(response, navigate);
+ onSuccess();
+ })
.catch((errorObj) => {
let errorMessage;
const isThereAnyErrorsArray = errorObj?.error?.length && typeof errorObj?.error?.[0] === 'string';
@@ -54,6 +56,7 @@ const SignupPage = ({ configs, organizationId }) => {
errorMessage = errorObj?.error?.error;
}
errorMessage && toast.error(errorMessage);
+ onFailure();
});
} else {
authenticationService
@@ -69,7 +72,7 @@ const SignupPage = ({ configs, organizationId }) => {
toast.error(e?.error || 'Something went wrong!', {
position: 'top-center',
});
- onFaluire();
+ onFailure();
});
}
};
diff --git a/frontend/src/modules/onboarding/pages/SignupPage/components/SignupForm/SignupForm.jsx b/frontend/src/modules/onboarding/pages/SignupPage/components/SignupForm/SignupForm.jsx
index 40eb34c61c..52e2d587cb 100644
--- a/frontend/src/modules/onboarding/pages/SignupPage/components/SignupForm/SignupForm.jsx
+++ b/frontend/src/modules/onboarding/pages/SignupPage/components/SignupForm/SignupForm.jsx
@@ -118,9 +118,11 @@ const SignupForm = ({
onSubmit(
formData,
() => {
+ // Success callback
setIsLoading(false);
},
() => {
+ // Error callback
setIsLoading(false);
}
);
diff --git a/frontend/src/modules/onboarding/stores/onboardingStore/onboarding.store.ce.js b/frontend/src/modules/onboarding/stores/onboardingStore/onboarding.store.ce.js
index ef3ebf2186..93dba3db13 100644
--- a/frontend/src/modules/onboarding/stores/onboardingStore/onboarding.store.ce.js
+++ b/frontend/src/modules/onboarding/stores/onboardingStore/onboarding.store.ce.js
@@ -21,6 +21,7 @@ const useCEOnboardingStore = create(
currentStep: 0,
totalSteps: 1,
accountCreated: false,
+ isOnboardingStepsCompleted: false,
// Action to update admin details
setAdminDetails: (details) =>
@@ -40,6 +41,7 @@ const useCEOnboardingStore = create(
// action to set current step
setCurrentStep: (step) => set({ currentStep: step }),
+ setOnboardingStepsCompleted: () => set({ isOnboardingStepsCompleted: true }),
// Action to reset the store
resetStore: () =>
set({
@@ -75,7 +77,8 @@ const useCEOnboardingStore = create(
const path = getSubpath()
? `${getSubpath()}/${session?.current_organization_slug}/apps/${appId}`
: `/${session?.current_organization_slug}/apps/${appId}`;
- window.location.href = path;
+ history.pushState(null, null, path);
+ window.location.reload();
},
setAccountCreated: (value) => set({ accountCreated: value }),
diff --git a/plugins/packages/mssql/lib/index.ts b/plugins/packages/mssql/lib/index.ts
index e9864a608c..84263a93a2 100644
--- a/plugins/packages/mssql/lib/index.ts
+++ b/plugins/packages/mssql/lib/index.ts
@@ -10,12 +10,16 @@ import {
import { SourceOptions, QueryOptions } from './types';
import { isEmpty } from '@tooljet-plugins/common';
-const STATEMENT_TIMEOUT = 10000;
-
export default class MssqlQueryService implements QueryService {
private static _instance: MssqlQueryService;
+ private STATEMENT_TIMEOUT;
constructor() {
+ this.STATEMENT_TIMEOUT =
+ process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT && !isNaN(Number(process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT))
+ ? Number(process.env.PLUGINS_SQL_DB_STATEMENT_TIMEOUT)
+ : 120000;
+
if (MssqlQueryService._instance) {
return MssqlQueryService._instance;
}
@@ -84,13 +88,13 @@ export default class MssqlQueryService implements QueryService {
private async executeQuery(knexInstance: Knex, query: string, sanitizedQueryParams: Record = {}) {
if (isEmpty(query)) throw new Error('Query is empty');
- const result = await knexInstance.raw(query, sanitizedQueryParams).timeout(STATEMENT_TIMEOUT);
+ const result = await knexInstance.raw(query, sanitizedQueryParams).timeout(this.STATEMENT_TIMEOUT);
return result;
}
async testConnection(sourceOptions: SourceOptions): Promise {
const knexInstance = await this.getConnection(sourceOptions, {}, false);
- await knexInstance.raw('select @@version;').timeout(STATEMENT_TIMEOUT);
+ await knexInstance.raw('select @@version;').timeout(this.STATEMENT_TIMEOUT);
knexInstance.destroy();
return {
diff --git a/plugins/packages/mysql/lib/index.ts b/plugins/packages/mysql/lib/index.ts
index 9fc9e9afac..8a4509a894 100644
--- a/plugins/packages/mysql/lib/index.ts
+++ b/plugins/packages/mysql/lib/index.ts
@@ -10,12 +10,16 @@ import {
import { SourceOptions, QueryOptions } from './types';
import { isEmpty } from '@tooljet-plugins/common';
-const STATEMENT_TIMEOUT = 10000;
-
export default class MysqlQueryService implements QueryService {
private static _instance: MysqlQueryService;
+ private STATEMENT_TIMEOUT;
constructor() {
+ this.STATEMENT_TIMEOUT =
+ process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT && !isNaN(Number(process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT))
+ ? Number(process.env.PLUGINS_SQL_DB_STATEMENT_TIMEOUT)
+ : 120000;
+
if (MysqlQueryService._instance) {
return MysqlQueryService._instance;
}
@@ -51,7 +55,7 @@ export default class MysqlQueryService implements QueryService {
async testConnection(sourceOptions: SourceOptions): Promise {
const knexInstance = await this.getConnection(sourceOptions, {}, false);
- await knexInstance.raw('select @@version;').timeout(STATEMENT_TIMEOUT);
+ await knexInstance.raw('select @@version;').timeout(this.STATEMENT_TIMEOUT);
knexInstance.destroy();
return { status: 'ok' };
}
@@ -77,7 +81,7 @@ export default class MysqlQueryService implements QueryService {
private async executeQuery(knexInstance: Knex, query: string, sanitizedQueryParams: Record = {}) {
if (isEmpty(query)) throw new Error('Query is empty');
- const result = await knexInstance.raw(query, sanitizedQueryParams).timeout(STATEMENT_TIMEOUT);
+ const result = await knexInstance.raw(query, sanitizedQueryParams).timeout(this.STATEMENT_TIMEOUT);
return result;
}
diff --git a/plugins/packages/postgresql/lib/index.ts b/plugins/packages/postgresql/lib/index.ts
index 78a5b9320e..fcbf2d736b 100644
--- a/plugins/packages/postgresql/lib/index.ts
+++ b/plugins/packages/postgresql/lib/index.ts
@@ -10,12 +10,16 @@ import { SourceOptions, QueryOptions } from './types';
import knex, { Knex } from 'knex';
import { isEmpty } from '@tooljet-plugins/common';
-const STATEMENT_TIMEOUT = 10000;
-
export default class PostgresqlQueryService implements QueryService {
private static _instance: PostgresqlQueryService;
+ private STATEMENT_TIMEOUT;
constructor() {
+ this.STATEMENT_TIMEOUT =
+ process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT && !isNaN(Number(process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT))
+ ? Number(process.env.PLUGINS_SQL_DB_STATEMENT_TIMEOUT)
+ : 120000;
+
if (PostgresqlQueryService._instance) {
return PostgresqlQueryService._instance;
}
@@ -51,7 +55,7 @@ export default class PostgresqlQueryService implements QueryService {
async testConnection(sourceOptions: SourceOptions): Promise {
const knexInstance = await this.getConnection(sourceOptions, {}, false);
- await knexInstance.raw('SELECT version();').timeout(STATEMENT_TIMEOUT);
+ await knexInstance.raw('SELECT version();').timeout(this.STATEMENT_TIMEOUT);
return { status: 'ok' };
}
@@ -75,9 +79,7 @@ export default class PostgresqlQueryService implements QueryService {
private async executeQuery(knexInstance: Knex, query: string, sanitizedQueryParams: Record = {}) {
if (isEmpty(query)) throw new Error('Query is empty');
-
- const { rows } = await knexInstance.raw(query, sanitizedQueryParams).timeout(STATEMENT_TIMEOUT);
-
+ const { rows } = await knexInstance.raw(query, sanitizedQueryParams);
return rows;
}
@@ -100,6 +102,7 @@ export default class PostgresqlQueryService implements QueryService {
password: sourceOptions.password,
port: sourceOptions.port,
ssl: this.getSslConfig(sourceOptions),
+ statement_timeout: this.STATEMENT_TIMEOUT,
};
} else if (sourceOptions.connection_type === 'string' && sourceOptions.connection_string) {
connectionConfig = {
@@ -111,7 +114,7 @@ export default class PostgresqlQueryService implements QueryService {
client: 'pg',
connection: connectionConfig,
pool: { min: 0, max: 10, acquireTimeoutMillis: 10000 },
- acquireConnectionTimeout: 10000,
+ acquireConnectionTimeout: 60000,
...this.connectionOptions(sourceOptions),
};
diff --git a/server/.version b/server/.version
index fe96cad5cf..2ae831adb3 100644
--- a/server/.version
+++ b/server/.version
@@ -1 +1 @@
-3.0.0-ce
+3.0.4-ce-lts
diff --git a/server/package.json b/server/package.json
index 745a0e9610..4f66f7de3d 100644
--- a/server/package.json
+++ b/server/package.json
@@ -53,6 +53,8 @@
"@sentry/tracing": "6.17.6",
"@tooljet/plugins": "../plugins",
"@types/express-serve-static-core": "^4.19.5",
+ "acorn": "^8.13.0",
+ "acorn-walk": "^8.3.4",
"bcrypt": "^5.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
diff --git a/server/src/controllers/app.controller.ts b/server/src/controllers/app.controller.ts
index 02f3534498..05f88d4ac2 100644
--- a/server/src/controllers/app.controller.ts
+++ b/server/src/controllers/app.controller.ts
@@ -73,7 +73,6 @@ export class AppController {
return await this.authService.validateInvitedUserSession(user, invitedUser, tokens);
}
- @UseGuards(SignupDisableGuard)
@UseGuards(FirstUserSignupDisableGuard)
@Post('activate-account-with-token')
async activateAccountWithToken(
@@ -164,7 +163,6 @@ export class AppController {
return await this.authService.acceptOrganizationInvite(response, user, acceptInviteDto);
}
- @UseGuards(SignupDisableGuard)
@UseGuards(FirstUserSignupDisableGuard)
@Post('signup')
async signup(@Body() appSignUpDto: AppSignupDto, @Res({ passthrough: true }) response: Response) {
diff --git a/server/src/controllers/organization_constants.controller.ts b/server/src/controllers/organization_constants.controller.ts
index e2d8023a5f..a1120c3942 100644
--- a/server/src/controllers/organization_constants.controller.ts
+++ b/server/src/controllers/organization_constants.controller.ts
@@ -59,7 +59,7 @@ export class OrganizationConstantController {
@Get('public/:app_slug')
async getConstantsFromPublicApp(@App() app) {
const result = await this.organizationConstantsService.allEnvironmentConstants(
- app.OrganizationId,
+ app.organizationId,
false,
OrganizationConstantType.GLOBAL
);
diff --git a/server/src/helpers/components.helper.ts b/server/src/helpers/components.helper.ts
index a9aea3403e..f59eed972c 100644
--- a/server/src/helpers/components.helper.ts
+++ b/server/src/helpers/components.helper.ts
@@ -36,11 +36,13 @@ export const buildComponentMetaDefinition = (components = {}) => {
componentMeta.definition.properties,
currentComponentData?.component?.definition?.properties,
(objValue, srcValue) => {
- if (
- ['Table', 'DropdownV2', 'MultiselectV2'].includes(currentComponentData?.component?.component) &&
+ if (['Table'].includes(currentComponentData?.component?.component) && isArray(objValue)) {
+ return srcValue;
+ } else if (
+ ['DropdownV2', 'MultiselectV2'].includes(currentComponentData?.component?.component) &&
isArray(objValue)
) {
- return srcValue;
+ return isArray(srcValue) ? srcValue : Object.values(srcValue);
}
}
),
diff --git a/server/src/helpers/import_export.helpers.ts b/server/src/helpers/import_export.helpers.ts
index 2c98071159..1f1bc57b7e 100644
--- a/server/src/helpers/import_export.helpers.ts
+++ b/server/src/helpers/import_export.helpers.ts
@@ -1,39 +1,40 @@
+import * as acorn from 'acorn';
+import * as walk from 'acorn-walk';
+
+function findExpression(input) {
+ const matches = [];
+ let startIdx = -1;
+ let braceCount = 0;
+
+ for (let i = 0; i < input.length; i++) {
+ if (input[i] === '{' && input[i + 1] === '{' && braceCount === 0) {
+ startIdx = i;
+ braceCount = 2;
+ i++; // Skip the second '{'
+ } else if (input[i] === '{' && braceCount > 0) {
+ braceCount++;
+ } else if (input[i] === '}' && braceCount > 0) {
+ braceCount--;
+ if (braceCount === 0 && startIdx !== -1) {
+ matches.push({
+ fullMatch: input.slice(startIdx, i + 1),
+ expression: input.slice(startIdx + 2, i - 1).trim(),
+ index: startIdx,
+ });
+ startIdx = -1;
+ }
+ }
+ }
+
+ return matches;
+}
+
export function updateEntityReferences(node, resourceMapping: Record = {}) {
if (typeof node === 'object') {
for (const key in node) {
let value = node[key];
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
- const referenceExists = value;
-
- if (referenceExists) {
- const matches = value.match(/{{(.*?)}}/g);
- // gett all references {{entityName}}
- if (matches) {
- matches.forEach((match) => {
- // remove curly braces and extract the entity "component.entityName.something"
- const ref = match.slice(2, -2).trim();
- const entityName = ref.split('.')[1];
-
- if (resourceMapping[entityName]) {
- const newValue = value.replace(entityName, resourceMapping[entityName]);
-
- node[key] = newValue;
- value = newValue;
- }
- });
- } else {
- // kept this logic for fallback, although it should not be needed
- const ref = value.replace('{{', '').replace('}}', '');
-
- const entityName = ref.split('.')[1];
-
- if (resourceMapping[entityName]) {
- const newValue = value.replace(entityName, resourceMapping[entityName]);
-
- node[key] = newValue;
- }
- }
- }
+ node[key] = extractAndReplaceReferencesFromString(value, resourceMapping, resourceMapping)?.valueWithId;
} else if (typeof value === 'object') {
value = updateEntityReferences(value, resourceMapping);
}
@@ -43,53 +44,373 @@ export function updateEntityReferences(node, resourceMapping: Record {
- const ref = match.slice(2, -2).trim(); // Remove {{ and }}
- const entityName = ref.split('.')[1];
- if (entityName && !allRefs.includes(entityName)) {
- allRefs.push(entityName);
- }
- });
- } else {
- // kept this logic for fallback, although it should not be needed
- const ref = value.replace('{{', '').replace('}}', '');
-
- const entityName = ref.split('.')[1];
-
- allRefs.push(entityName);
- }
- }
- } else if (typeof value === 'object') {
- findAllEntityReferences(value, allRefs);
- }
- }
- }
- return allRefs;
-}
-
export function isValidUUID(uuid) {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(uuid);
}
+
+export function extractAndReplaceReferencesFromString(input, componentIdNameMapping = {}, queryIdNameMapping = {}) {
+ // Quick check for relevant keywords
+ const regexForQuickCheck =
+ /\b(components|queries|globals|variables|page|parameters|secrets|constants)(?:\[\S*|\.\S*|\?\.\S*)/;
+ if (!regexForQuickCheck.test(input)) {
+ return {
+ allRefs: [],
+ valueWithId: input,
+ valueWithBrackets: input,
+ };
+ }
+
+ const relevantKeywords = /\b(components|queries|globals|variables|page|parameters|secrets|constants)\b/;
+ const expressionRegex = /{{(.*?)}}/gs;
+ const results = [];
+ let lastIndex = 0;
+ let replacedString = '';
+ let bracketNotationString = '';
+
+ // Precompile the UUID regex
+ const uuidRegex =
+ /\b(components|queries)(\??\.|\??\.?\[['"]?)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(['"]?\])?/g;
+
+ let match;
+ if (input.startsWith('{{{') && input.endsWith('}}}')) {
+ const inputContent = input.slice(3, -3);
+ input = `{{({${inputContent}})}}`;
+ const matches = findExpression(input);
+ for (const match of matches) {
+ const { fullMatch, expression, index } = match;
+
+ // Check if the expression contains relevant keywords
+ if (!relevantKeywords.test(expression)) {
+ replacedString += input.slice(lastIndex, index);
+ bracketNotationString += input.slice(lastIndex, index);
+ replacedString += fullMatch;
+ bracketNotationString += fullMatch;
+ lastIndex = index + fullMatch.length;
+ continue;
+ }
+
+ try {
+ const { processedExpression, uuidMappings } = preprocessExpression(
+ expression,
+ uuidRegex,
+ componentIdNameMapping,
+ queryIdNameMapping
+ );
+ const parsedResult = parseExpression(
+ processedExpression,
+ componentIdNameMapping,
+ queryIdNameMapping,
+ uuidMappings
+ );
+
+ replacedString += input.slice(lastIndex, index);
+ bracketNotationString += input.slice(lastIndex, index);
+
+ const replacedExpression = replaceIdsInExpression(
+ processedExpression,
+ componentIdNameMapping,
+ queryIdNameMapping,
+ false,
+ uuidMappings
+ );
+ const bracketNotationExpression = replaceIdsInExpression(
+ processedExpression,
+ componentIdNameMapping,
+ queryIdNameMapping,
+ true,
+ uuidMappings
+ );
+
+ replacedString += `{{${replacedExpression}}}`;
+ bracketNotationString += `{{${bracketNotationExpression}}}`;
+
+ results.push({
+ allRefs: parsedResult.references,
+ valueWithId: `{{${replacedExpression}}}`,
+ valueWithBrackets: `{{${bracketNotationExpression}}}`,
+ });
+ } catch (error) {
+ replacedString += fullMatch;
+ bracketNotationString += fullMatch;
+ results.push({
+ allRefs: [],
+ valueWithId: fullMatch,
+ valueWithBrackets: fullMatch,
+ });
+ }
+
+ lastIndex = index + fullMatch.length;
+ }
+
+ replacedString += input.slice(lastIndex);
+ bracketNotationString += input.slice(lastIndex);
+ // remove the parentheses that were added
+
+ return {
+ valueWithId: `{{${replacedString.slice(3, -3)}}}`,
+ valueWithBrackets: `{{${bracketNotationString.slice(3, -3)}}}`,
+ allRefs: results.flatMap((r) => r.allRefs),
+ };
+ }
+ while ((match = expressionRegex.exec(input)) !== null) {
+ const fullMatch = match[0];
+ const expression = match[1].trim();
+
+ // Check if the expression contains relevant keywords
+ if (!relevantKeywords.test(expression)) {
+ replacedString += input.slice(lastIndex, match.index);
+ bracketNotationString += input.slice(lastIndex, match.index);
+ replacedString += fullMatch;
+ bracketNotationString += fullMatch;
+ lastIndex = match.index + fullMatch.length;
+ continue;
+ }
+
+ try {
+ const { processedExpression, uuidMappings } = preprocessExpression(
+ expression,
+ uuidRegex,
+ componentIdNameMapping,
+ queryIdNameMapping
+ );
+ const parsedResult = parseExpression(
+ processedExpression,
+ componentIdNameMapping,
+ queryIdNameMapping,
+ uuidMappings
+ );
+
+ replacedString += input.slice(lastIndex, match.index);
+ bracketNotationString += input.slice(lastIndex, match.index);
+
+ const replacedExpression = replaceIdsInExpression(
+ processedExpression,
+ componentIdNameMapping,
+ queryIdNameMapping,
+ false,
+ uuidMappings
+ );
+ const bracketNotationExpression = replaceIdsInExpression(
+ processedExpression,
+ componentIdNameMapping,
+ queryIdNameMapping,
+ true,
+ uuidMappings
+ );
+
+ replacedString += `{{${replacedExpression}}}`;
+ bracketNotationString += `{{${bracketNotationExpression}}}`;
+
+ results.push({
+ allRefs: parsedResult.references,
+ valueWithId: `{{${replacedExpression}}}`,
+ valueWithBrackets: `{{${bracketNotationExpression}}}`,
+ });
+ } catch (error) {
+ replacedString += fullMatch;
+ bracketNotationString += fullMatch;
+ results.push({
+ allRefs: [],
+ valueWithId: fullMatch,
+ valueWithBrackets: fullMatch,
+ });
+ }
+
+ lastIndex = match.index + fullMatch.length;
+ }
+
+ replacedString += input.slice(lastIndex);
+ bracketNotationString += input.slice(lastIndex);
+
+ return {
+ allRefs: results.flatMap((r) => r.allRefs),
+ valueWithId: replacedString,
+ valueWithBrackets: bracketNotationString,
+ };
+}
+
+function preprocessExpression(expression, uuidRegex, componentIdNameMapping, queryIdNameMapping) {
+ const uuidMappings = {};
+ let placeholderCounter = 0;
+
+ const processedExpression = expression.replace(uuidRegex, (match, p1, p2, p3, p4) => {
+ const placeholder = `__UUID_PLACEHOLDER_${placeholderCounter}__`;
+ uuidMappings[placeholder] = (p1 === 'components' ? componentIdNameMapping[p3] : queryIdNameMapping[p3]) || p3;
+ placeholderCounter++;
+ return `${p1}${p2}${placeholder}${p4 || ''}`;
+ });
+
+ return { processedExpression, uuidMappings };
+}
+
+function replaceIdsInExpression(
+ expression,
+ componentIdNameMapping,
+ queryIdNameMapping,
+ useBracketNotation,
+ uuidMappings
+) {
+ try {
+ const ast = acorn.parse(expression, { ecmaVersion: 2020 });
+ const replacements = [];
+
+ walk.simple(ast, {
+ MemberExpression(node) {
+ if (
+ node.object.type === 'Identifier' &&
+ (node.object.name === 'components' || node.object.name === 'queries')
+ ) {
+ const isComponent = node.object.name === 'components';
+ const mapping = isComponent ? componentIdNameMapping : queryIdNameMapping;
+
+ if (node.property.type === 'Identifier') {
+ const name = node.property.name;
+ const nameWithOptionalCheck = node.optional
+ ? useBracketNotation
+ ? `${node.object.name}?.`
+ : `${node.object.name}?`
+ : `${node.object.name}`;
+ if (mapping[name] || name.startsWith('__UUID_PLACEHOLDER_')) {
+ const start = node.start;
+ const end = node.end;
+ let replacement;
+ if (name.startsWith('__UUID_PLACEHOLDER_')) {
+ const actualName = uuidMappings[name];
+ replacement = useBracketNotation
+ ? `${nameWithOptionalCheck}["${actualName}"]`
+ : `${nameWithOptionalCheck}.${actualName}`;
+ } else {
+ replacement = useBracketNotation
+ ? `${nameWithOptionalCheck}["${mapping[name]}"]`
+ : `${nameWithOptionalCheck}.${mapping[name]}`;
+ }
+ replacements.push({ start, end, replacement });
+ }
+ } else if (node.property.type === 'Literal') {
+ const name = node.property.value as string;
+ const nameWithOptionalCheck = node.optional ? `${node.object.name}?.` : `${node.object.name}`;
+ if (mapping[name] || name.startsWith('__UUID_PLACEHOLDER_')) {
+ const start = node.start;
+ const end = node.end;
+ let replacement;
+ if (name.startsWith('__UUID_PLACEHOLDER_')) {
+ const actualName = uuidMappings[name];
+ replacement = `${nameWithOptionalCheck}["${actualName}"]`;
+ } else {
+ replacement = `${nameWithOptionalCheck}["${mapping[name]}"]`;
+ }
+ replacements.push({ start, end, replacement });
+ }
+ }
+ }
+ },
+ });
+
+ if (replacements.length === 0) return expression;
+
+ replacements.sort((a, b) => b.start - a.start);
+
+ let result = expression;
+ for (const { start, end, replacement } of replacements) {
+ result = result.slice(0, start) + replacement + result.slice(end);
+ }
+
+ return result;
+ } catch (error) {
+ return expression;
+ }
+}
+
+function parseExpression(expression, componentIdNameMapping, queryIdNameMapping, uuidMappings) {
+ try {
+ const ast = acorn.parse(expression, { ecmaVersion: 2020 });
+ const references = [];
+ const validRootObjects = {
+ components: true,
+ queries: true,
+ variables: true,
+ globals: true,
+ page: true,
+ };
+ walk.simple(ast, {
+ MemberExpression: handleMemberExpression,
+ });
+
+ // eslint-disable-next-line no-inner-declarations
+ function handleMemberExpression(node) {
+ const reference = extractPath(node);
+ if (reference) references.push(reference);
+ }
+
+ // eslint-disable-next-line no-inner-declarations
+ function extractPath(node) {
+ const path = [];
+ let current = node;
+ let rootObject = '';
+
+ while (current) {
+ if (current.type === 'Identifier') {
+ path.unshift(current.name);
+ if (validRootObjects[current.name]) {
+ rootObject = current.name;
+ break;
+ }
+ } else if (current.type === 'MemberExpression' || current.type === 'OptionalMemberExpression') {
+ if (current.computed) {
+ if (
+ current.property.type === 'Literal' &&
+ (typeof current.property.value === 'string' || typeof current.property.value === 'number')
+ ) {
+ path.unshift(current.property.value.toString());
+ } else {
+ break;
+ }
+ } else {
+ path.unshift(current.property.name);
+ }
+ } else {
+ break;
+ }
+ current = current.object;
+ }
+
+ if (
+ (rootObject && (rootObject === 'queries' || rootObject === 'components') && path.length >= 3) ||
+ ((rootObject === 'variables' || rootObject === 'globals') && path.length === 2) ||
+ (rootObject === 'page' && path.length === 3)
+ ) {
+ return createReferenceObject(rootObject, path, uuidMappings, componentIdNameMapping, queryIdNameMapping);
+ }
+ return null;
+ }
+
+ return { references };
+ } catch (error) {
+ console.log(error);
+ return { references: [] };
+ }
+}
+
+function createReferenceObject(entityType, path, uuidMappings, componentIdNameMapping, queryIdNameMapping) {
+ let entityNameOrId, entityKey;
+
+ if (entityType === 'components' || entityType === 'queries') {
+ entityNameOrId = path[1];
+ entityKey = path[2];
+
+ if (entityNameOrId.startsWith('__UUID_PLACEHOLDER_')) {
+ entityNameOrId = uuidMappings[entityNameOrId];
+ } else {
+ const mapping = entityType === 'components' ? componentIdNameMapping : queryIdNameMapping;
+ entityNameOrId = mapping[entityNameOrId] || entityNameOrId;
+ }
+ } else if (entityType === 'variables' || entityType === 'globals') {
+ entityKey = path[1];
+ } else if (entityType === 'page') {
+ entityNameOrId = path[1];
+ entityKey = path[2];
+ }
+
+ return { entityType, entityNameOrId, entityKey };
+}
diff --git a/server/src/helpers/utils.helper.ts b/server/src/helpers/utils.helper.ts
index 72c64c0c37..41dde11284 100644
--- a/server/src/helpers/utils.helper.ts
+++ b/server/src/helpers/utils.helper.ts
@@ -424,3 +424,13 @@ export function mergeDeep(target, source, seen = new WeakMap()) {
return target;
}
+export const getSubpath = () => {
+ const subpath = process.env.SUB_PATH || '';
+ // Ensure subpath starts and ends with slashes
+ if (subpath) {
+ if (!subpath.startsWith('/') || !subpath.endsWith('/')) {
+ throw new Error('SUB_PATH must start and end with a slash');
+ }
+ }
+ return subpath;
+};
\ No newline at end of file
diff --git a/server/src/helpers/widget-config/buttonGroup.js b/server/src/helpers/widget-config/buttonGroup.js
index 65b7e77807..c0fa889dd5 100644
--- a/server/src/helpers/widget-config/buttonGroup.js
+++ b/server/src/helpers/widget-config/buttonGroup.js
@@ -146,8 +146,8 @@ export const buttonGroupConfig = {
visibility: { value: '{{true}}' },
borderRadius: { value: '{{4}}' },
disabledState: { value: '{{false}}' },
- selectedTextColor: { value: '' },
- selectedBackgroundColor: { value: '' },
+ selectedTextColor: { value: '#FFFFFF' },
+ selectedBackgroundColor: { value: '#4368E3' },
},
},
};
diff --git a/server/src/helpers/widget-config/codeEditor.js b/server/src/helpers/widget-config/codeEditor.js
index d7ff03d9a8..eb76891af0 100644
--- a/server/src/helpers/widget-config/codeEditor.js
+++ b/server/src/helpers/widget-config/codeEditor.js
@@ -67,6 +67,13 @@ export const codeEditorConfig = {
exposedVariables: {
value: '',
},
+ actions: [
+ {
+ handle: 'setValue',
+ displayName: 'Set value',
+ params: [{ handle: 'setValue', defaultValue: '' }],
+ },
+ ],
definition: {
others: {
showOnDesktop: { value: '{{true}}' },
diff --git a/server/src/main.ts b/server/src/main.ts
index 0efcbbfd18..f7a129ff86 100644
--- a/server/src/main.ts
+++ b/server/src/main.ts
@@ -13,6 +13,7 @@ import { bootstrap as globalAgentBootstrap } from 'global-agent';
import { join } from 'path';
import * as helmet from 'helmet';
import * as express from 'express';
+import { getSubpath } from '@helpers/utils.helper';
const fs = require('fs');
@@ -100,7 +101,15 @@ function setSecurityHeaders(app, configService) {
app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'geolocation=(self), camera=(), microphone=()');
- res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+
+ const subpath = getSubpath();
+ const path = req.path.replace(subpath, subpath ? '/' : '');
+ if (path.startsWith('/api/')) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ } else {
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
+ }
+
return next();
});
}
diff --git a/server/src/services/app_import_export.service.ts b/server/src/services/app_import_export.service.ts
index 17a9d2f99f..e799047435 100644
--- a/server/src/services/app_import_export.service.ts
+++ b/server/src/services/app_import_export.service.ts
@@ -1,5 +1,5 @@
import { BadRequestException, Injectable } from '@nestjs/common';
-import { isEmpty } from 'lodash';
+import { isEmpty, set } from 'lodash';
import { App } from 'src/entities/app.entity';
import { AppEnvironment } from 'src/entities/app_environments.entity';
import { AppVersion } from 'src/entities/app_version.entity';
@@ -30,7 +30,7 @@ import { Component } from 'src/entities/component.entity';
import { Layout } from 'src/entities/layout.entity';
import { EventHandler, Target } from 'src/entities/event_handler.entity';
import { v4 as uuid } from 'uuid';
-import { findAllEntityReferences, isValidUUID, updateEntityReferences } from 'src/helpers/import_export.helpers';
+import { updateEntityReferences } from 'src/helpers/import_export.helpers';
interface AppResourceMappings {
defaultDataSourceIdMapping: Record;
dataQueryMapping: Record;
@@ -290,13 +290,7 @@ export class AppImportExportService {
.getMany();
const toUpdateComponents = components.filter((component) => {
- const entityReferencesInComponentDefinitions = findAllEntityReferences(component, []).filter(
- (entity) => entity && isValidUUID(entity)
- );
-
- if (entityReferencesInComponentDefinitions.length > 0) {
- return updateEntityReferences(component, mappings);
- }
+ return updateEntityReferences(component, mappings);
});
if (!isEmpty(toUpdateComponents)) {
@@ -312,19 +306,27 @@ export class AppImportExportService {
.getMany();
const toUpdateDataQueries = dataQueries.filter((dataQuery) => {
- const entityReferencesInQueryOptions = findAllEntityReferences(dataQuery, []).filter(
- (entity) => entity && isValidUUID(entity)
- );
-
- if (entityReferencesInQueryOptions.length > 0) {
- return updateEntityReferences(dataQuery, mappings);
- }
+ return updateEntityReferences(dataQuery, mappings);
});
if (!isEmpty(toUpdateDataQueries)) {
await manager.save(toUpdateDataQueries);
}
}
+ // update Global settings of created versions
+ const appVersionIds = Object.values(resourceMapping.appVersionMapping);
+ const newAppVersions = await manager.find(AppVersion, {
+ where: {
+ id: In(appVersionIds),
+ },
+ select: ['id', 'globalSettings'],
+ });
+ for (const appVersion of newAppVersions) {
+ if (appVersion.globalSettings) {
+ const updatedGlobalSettings = updateEntityReferences(appVersion.globalSettings, mappings);
+ await manager.update(AppVersion, { id: appVersion.id }, { globalSettings: updatedGlobalSettings });
+ }
+ }
}
async createImportedAppForUser(manager: EntityManager, appParams: any, user: User, isGitApp = false): Promise {
@@ -503,6 +505,7 @@ export class AppImportExportService {
autoComputeLayout: page.autoComputeLayout || false,
isPageGroup: page.isPageGroup || false,
pageGroupIndex: page.pageGroupIndex || null,
+ icon: page.icon || null,
});
const pageCreated = await transactionalEntityManager.save(newPage);
@@ -770,6 +773,7 @@ export class AppImportExportService {
disabled: page.disabled || false,
hidden: page.hidden || false,
autoComputeLayout: page.autoComputeLayout || false,
+ icon: page.icon || null,
});
const pageCreated = await manager.save(newPage);
@@ -795,6 +799,10 @@ export class AppImportExportService {
const newComponent = new Component();
let parentId = component.parent ? component.parent : null;
+ if (component?.properties?.buttonToSubmit) {
+ const newButtonToSubmitValue = newComponentIdsMap[component?.properties?.buttonToSubmit?.value];
+ if (newButtonToSubmitValue) set(component, 'properties.buttonToSubmit.value', newButtonToSubmitValue);
+ }
const isParentTabOrCalendar = isChildOfTabsOrCalendar(component, pageComponents, parentId, true);
@@ -1260,6 +1268,23 @@ export class AppImportExportService {
return appResourceMappings;
}
+ createViewerNavigationVisibilityForImportedApp(importedVersion: AppVersion) {
+ let pageSettings = {};
+ if (importedVersion.pageSettings) {
+ pageSettings = { ...importedVersion.pageSettings };
+ } else {
+ pageSettings = {
+ properties: {
+ disableMenu: {
+ value: `{{${!importedVersion.showViewerNavigation}}}`,
+ fxActive: false,
+ },
+ },
+ };
+ }
+ return pageSettings;
+ }
+
async createAppVersionsForImportedApp(
manager: EntityManager,
user: User,
@@ -1300,7 +1325,7 @@ export class AppImportExportService {
version.showViewerNavigation = appVersion.showViewerNavigation;
version.homePageId = appVersion.homePageId;
version.globalSettings = appVersion.globalSettings;
- version.pageSettings = appVersion.pageSettings;
+ version.pageSettings = this.createViewerNavigationVisibilityForImportedApp(appVersion);
} else {
version.showViewerNavigation = appVersion.definition?.showViewerNavigation || true;
version.homePageId = appVersion.definition?.homePageId;
@@ -1318,7 +1343,7 @@ export class AppImportExportService {
};
} else {
version.globalSettings = appVersion.definition?.globalSettings;
- version.pageSettings = appVersion.definition?.pageSettings;
+ version.pageSettings = this.createViewerNavigationVisibilityForImportedApp(appVersion);
}
}
diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts
index 60e5529751..cd683968b8 100644
--- a/server/src/services/apps.service.ts
+++ b/server/src/services/apps.service.ts
@@ -27,8 +27,8 @@ import { Component } from 'src/entities/component.entity';
import { EventHandler, Target } from 'src/entities/event_handler.entity';
import { VersionReleaseDto } from '@dto/version-release.dto';
-import { findAllEntityReferences, isValidUUID, updateEntityReferences } from 'src/helpers/import_export.helpers';
-import { isEmpty } from 'lodash';
+import { updateEntityReferences } from 'src/helpers/import_export.helpers';
+import { isEmpty, set } from 'lodash';
import { AppBase } from 'src/entities/app_base.entity';
import { LayoutDimensionUnits } from 'src/helpers/components.helper';
import { AbilityService } from './permissions-ability.service';
@@ -392,6 +392,15 @@ export class AppsService {
dataQueryMapping: oldDataQueryToNewMapping,
});
+ if (appVersion.globalSettings) {
+ const globalSettings = appVersion.globalSettings;
+ const updatedGlobalSettings = updateEntityReferences(globalSettings, {
+ ...oldDataQueryToNewMapping,
+ ...oldComponentToNewComponentMapping,
+ });
+ await manager.update(AppVersion, { id: appVersion.id }, { globalSettings: updatedGlobalSettings });
+ }
+
await this.updateEventActionsForNewVersionWithNewMappingIds(
manager,
appVersion.id,
@@ -426,13 +435,7 @@ export class AppsService {
.getMany();
const toUpdateComponents = components.filter((component) => {
- const entityReferencesInComponentDefinitions = findAllEntityReferences(component, []).filter(
- (entity) => entity && isValidUUID(entity)
- );
-
- if (entityReferencesInComponentDefinitions.length > 0) {
- return updateEntityReferences(component, mappings);
- }
+ return updateEntityReferences(component, mappings);
});
if (!isEmpty(toUpdateComponents)) {
@@ -448,13 +451,7 @@ export class AppsService {
.getMany();
const toUpdateDataQueries = dataQueries.filter((dataQuery) => {
- const entityReferencesInQueryOptions = findAllEntityReferences(dataQuery, []).filter(
- (entity) => entity && isValidUUID(entity)
- );
-
- if (entityReferencesInQueryOptions.length > 0) {
- return updateEntityReferences(dataQuery, mappings);
- }
+ return updateEntityReferences(dataQuery, mappings);
});
if (!isEmpty(toUpdateDataQueries)) {
@@ -651,9 +648,14 @@ export class AppsService {
await manager.save(newEvent);
});
});
-
newComponents.forEach((component) => {
let parentId = component.parent ? component.parent : null;
+ // re establish mapping relationship
+ if (component?.properties?.buttonToSubmit) {
+ const newButtonToSubmitValue =
+ oldComponentToNewComponentMapping[component?.properties?.buttonToSubmit?.value];
+ if (newButtonToSubmitValue) set(component, 'properties.buttonToSubmit.value', newButtonToSubmitValue);
+ }
if (!parentId) return;
diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts
index 863c417a65..5bcbbfc996 100644
--- a/server/src/services/auth.service.ts
+++ b/server/src/services/auth.service.ts
@@ -347,7 +347,7 @@ export class AuthService {
return dbTransactionWrap(async (manager: EntityManager) => {
// Check if the configs allows user signups
- if (this.configService.get('DISABLE_SIGNUPS') === 'true') {
+ if (!organizationId && this.configService.get('DISABLE_SIGNUPS') === 'true') {
throw new NotAcceptableException();
}
diff --git a/server/src/services/components.service.ts b/server/src/services/components.service.ts
index cbc728a105..1d436a6e72 100644
--- a/server/src/services/components.service.ts
+++ b/server/src/services/components.service.ts
@@ -97,13 +97,13 @@ export class ComponentsService {
componentData[column === 'others' ? 'displayPreferences' : column],
updatedDefinition[column],
(objValue, srcValue) => {
- if (
- (componentData.type === 'Table' ||
- componentData.type === 'DropdownV2' ||
- componentData.type === 'MultiselectV2') &&
+ if (componentData.type === 'Table' && _.isArray(objValue)) {
+ return srcValue;
+ } else if (
+ (componentData.type === 'DropdownV2' || componentData.type === 'MultiselectV2') &&
_.isArray(objValue)
) {
- return srcValue;
+ return _.isArray(srcValue) ? srcValue : Object.values(srcValue);
}
}
);
diff --git a/server/src/services/organizations.service.ts b/server/src/services/organizations.service.ts
index 0a539bd381..4101b9e1ef 100644
--- a/server/src/services/organizations.service.ts
+++ b/server/src/services/organizations.service.ts
@@ -146,7 +146,7 @@ export class OrganizationsService {
enable_sign_up: this.configService.get('DISABLE_SIGNUPS') !== 'true',
enabled: true,
},
- enableSignUp: this.configService.get('SSO_DISABLE_SIGNUPS') !== 'true',
+ enableSignUp: this.configService.get('DISABLE_SIGNUPS') !== 'true',
};
}
diff --git a/server/src/services/page.service.ts b/server/src/services/page.service.ts
index 81f2eddfe1..84a0db5ed5 100644
--- a/server/src/services/page.service.ts
+++ b/server/src/services/page.service.ts
@@ -11,9 +11,10 @@ import { EventsService } from './events_handler.service';
import { Component } from 'src/entities/component.entity';
import { Layout } from 'src/entities/layout.entity';
import { EventHandler } from 'src/entities/event_handler.entity';
-import { findAllEntityReferences, isValidUUID, updateEntityReferences } from 'src/helpers/import_export.helpers';
+import { updateEntityReferences } from 'src/helpers/import_export.helpers';
import { isEmpty } from 'class-validator';
import { PageHelperService } from '@apps/services/pages/service.helper';
+import * as _ from 'lodash';
@Injectable()
export class PageService {
@@ -101,19 +102,25 @@ export class PageService {
const componentsIdMap = {};
// Clone components
+ // array to store maapings and update them later with path
+ const mappingsToUpdate = [];
const clonedComponents = await Promise.all(
pageComponents.map(async (component) => {
const clonedComponent = { ...component, id: undefined, pageId: clonePageId };
const newComponent = await manager.save(manager.create(Component, clonedComponent));
-
componentsIdMap[component.id] = newComponent.id;
const componentLayouts = await manager.find(Layout, { where: { componentId: component.id } });
+ if (component?.properties?.buttonToSubmit?.value) {
+ mappingsToUpdate.push({
+ component: newComponent,
+ pathToUpdate: 'properties.buttonToSubmit.value',
+ });
+ }
const clonedLayouts = componentLayouts.map((layout) => ({
...layout,
id: undefined,
componentId: newComponent.id,
}));
-
// Clone component events
const clonedComponentEvents = await this.eventHandlerService.findAllEventsWithSourceId(component.id);
const clonedEvents = clonedComponentEvents.map((event) => {
@@ -150,7 +157,18 @@ export class PageService {
return newComponent;
})
);
-
+ // re estabilish mappings
+ await Promise.all(
+ mappingsToUpdate.map((itemToUpdate) => {
+ const { component, pathToUpdate: path } = itemToUpdate;
+ const oldId = _.get(component, path);
+ const newId = componentsIdMap[oldId];
+ if (newId) {
+ _.set(component, path, newId);
+ }
+ manager.save(component);
+ })
+ );
// Clone events
await Promise.all(
pageEvents.map(async (event) => {
@@ -225,13 +243,7 @@ export class PageService {
}
const toUpdateComponents = clonedComponents.filter((component) => {
- const entityReferencesInComponentDefinitions = findAllEntityReferences(component, []).filter(
- (entity) => entity && isValidUUID(entity)
- );
-
- if (entityReferencesInComponentDefinitions.length > 0) {
- return updateEntityReferences(component, componentsIdMap);
- }
+ return updateEntityReferences(component, componentsIdMap);
});
if (!isEmpty(toUpdateComponents)) {
|