diff --git a/.version b/.version index e7bb755881..99ba0ddace 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -3.0.1-ce-lts +3.0.2-ce-lts diff --git a/frontend/.version b/frontend/.version index e7bb755881..99ba0ddace 100644 --- a/frontend/.version +++ b/frontend/.version @@ -1 +1 @@ -3.0.1-ce-lts +3.0.2-ce-lts diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index f5603de836..a8a10caf55 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -596,7 +596,10 @@ export default function Grid({ gridWidth, currentLayout }) { isDragOnTableORCalendar = tableElem.contains(e.inputEvent.target); } if (box?.component?.component === 'Calendar') { - const calenderElem = e.target.querySelector('.rbc-month-view'); + const calenderElem = + e.target.querySelector('.rbc-month-view') || + e.target.querySelector('.rbc-time-view') || + e.target.querySelector('.rbc-day-view'); isDragOnTableORCalendar = calenderElem.contains(e.inputEvent.target); } diff --git a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx index cb3fc653c2..e7284767d1 100644 --- a/frontend/src/AppBuilder/Header/CreateVersionModal.jsx +++ b/frontend/src/AppBuilder/Header/CreateVersionModal.jsx @@ -201,8 +201,9 @@ export const CreateVersion = ({ showCreateAppVersion, setShowCreateAppVersion }) width: '100%', }} > + {/* EE - change to development */}
- The new version will be created in development environment + The new version will be created in production environment
diff --git a/frontend/src/AppBuilder/Widgets/Calendar/Calendar.jsx b/frontend/src/AppBuilder/Widgets/Calendar/Calendar.jsx index 080a51c47c..b7957d9ba0 100644 --- a/frontend/src/AppBuilder/Widgets/Calendar/Calendar.jsx +++ b/frontend/src/AppBuilder/Widgets/Calendar/Calendar.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Calendar as ReactCalendar, momentLocalizer } from 'react-big-calendar'; import moment from 'moment'; import 'react-big-calendar/lib/css/react-big-calendar.css'; @@ -54,7 +54,7 @@ export const Calendar = function ({ const [currentDate, setCurrentDate] = useState(defaultDate); const [eventPopoverOptions, setEventPopoverOptions] = useState({ show: false }); - const [defaultView, setDefaultValue] = useState(allowedCalendarViews[0]); + const isInitialRender = useRef(true); const eventPropGetter = (event) => { const backgroundColor = event.color; @@ -100,10 +100,10 @@ export const Calendar = function ({ const view = allowedCalendarViews.includes(properties.defaultView) ? properties.defaultView : allowedCalendarViews[0]; - if (currentView !== view) { - setDefaultValue(view); + if (currentView !== view || isInitialRender.current) { setExposedVariable('currentView', view); setCurrentView(view); + isInitialRender.current = false; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [properties.defaultView]); @@ -145,10 +145,9 @@ export const Calendar = function ({ endAccessor="end" style={style} views={allowedCalendarViews} - defaultView={defaultView} - view={defaultView} + defaultView={properties.defaultView || allowedCalendarViews[0]} + view={currentView} onView={(view) => { - setDefaultValue(view); setExposedVariable('currentView', view); setCurrentView(view); fireEvent('onCalendarViewChange'); diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index 38571c8c35..eeec8760fc 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -19,7 +19,6 @@ export const Form = function Form(props) { component, width, height, - removeComponent, styles, setExposedVariable, setExposedVariables, @@ -28,11 +27,6 @@ export const Form = function Form(props) { properties, resetComponent = () => {}, dataCy, - paramUpdated, - currentLayout, - mode, - getContainerProps, - containerProps, } = props; const childComponents = useStore((state) => state.getChildComponents(id), shallow); const { visibility, disabledState, borderRadius, borderColor, boxShadow } = styles; diff --git a/frontend/src/AppBuilder/Widgets/Form/RenderSchema.jsx b/frontend/src/AppBuilder/Widgets/Form/RenderSchema.jsx index bf4fc459c3..b5bfa9e4c3 100644 --- a/frontend/src/AppBuilder/Widgets/Form/RenderSchema.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/RenderSchema.jsx @@ -50,6 +50,7 @@ const RenderSchema = ({ component, parent, id, onOptionChange, onOptionsChange, darkMode={darkMode} fireEvent={fireEvent} formId={formId} + id={id} /> ); }; diff --git a/frontend/src/AppBuilder/Widgets/Table/Table.jsx b/frontend/src/AppBuilder/Widgets/Table/Table.jsx index 97fe46d94f..986edca4f5 100644 --- a/frontend/src/AppBuilder/Widgets/Table/Table.jsx +++ b/frontend/src/AppBuilder/Widgets/Table/Table.jsx @@ -237,7 +237,8 @@ export const Table = React.memo( const changesToBeSavedAndExposed = { dataUpdates: newDataUpdates, changeSet: newChangeset }; mergeToTableDetails(changesToBeSavedAndExposed); setExposedVariables({ ...changesToBeSavedAndExposed, updatedData: clonedTableData }); - fireEvent('onCellValueChanged'); + // Need to add a timeout here as changes are happening in the next render + setTimeout(() => fireEvent('onCellValueChanged'), 0); return; } diff --git a/frontend/src/AppBuilder/_stores/ast.js b/frontend/src/AppBuilder/_stores/ast.js index f735228974..106298588f 100644 --- a/frontend/src/AppBuilder/_stores/ast.js +++ b/frontend/src/AppBuilder/_stores/ast.js @@ -54,7 +54,8 @@ export function extractAndReplaceReferencesFromString(input, componentIdNameMapp let match; if (input.startsWith('{{{') && input.endsWith('}}}')) { - input = input.replace(/\{\{(.*)\}\}/, '{{($1)}}'); + const inputContent = input.slice(3, -3); + input = `{{({${inputContent}})}}`; const matches = findExpression(input); for (const match of matches) { const { fullMatch, expression, index } = match; diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index f7c64475c6..7fb9650075 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -1192,8 +1192,16 @@ export const createComponentsSlice = (set, get) => ({ 'setComponentProperty' ); + const oldComponent = get().modules[moduleId].pages[currentPageIndex].components[componentId].component; + const { events, exposedVariables, ...filteredDefinition } = oldComponent.definition || {}; + const diff = { - [componentId]: { component: get().modules[moduleId].pages[currentPageIndex].components[componentId].component }, + [componentId]: { + component: { + ...oldComponent, + definition: filteredDefinition, + }, + }, }; if (saveAfterAction) { @@ -1236,8 +1244,16 @@ export const createComponentsSlice = (set, get) => ({ ); } + const oldComponent = get().modules[moduleId].pages[currentPageIndex].components[componentId].component; + const { events, exposedVariables, ...filteredDefinition } = oldComponent.definition || {}; + const diff = { - [componentId]: { component: get().modules[moduleId].pages[currentPageIndex].components[componentId].component }, + [componentId]: { + component: { + ...oldComponent, + definition: filteredDefinition, + }, + }, }; if (saveAfterAction) { diff --git a/frontend/src/ManageOrgConstants/ManageOrgConstants.jsx b/frontend/src/ManageOrgConstants/ManageOrgConstants.jsx index b6548197bd..3d40ce168e 100644 --- a/frontend/src/ManageOrgConstants/ManageOrgConstants.jsx +++ b/frontend/src/ManageOrgConstants/ManageOrgConstants.jsx @@ -17,6 +17,7 @@ import { BreadCrumbContext } from '@/App'; import './ConstantFormStyle.scss'; import { Constants, redirectToWorkspace } from '@/_helpers/utils'; import { SearchBox } from '@/_components/SearchBox'; +import { OrganizationList } from '@/_components/OrganizationManager/List'; const MODES = Object.freeze({ CREATE: 'create', EDIT: 'edit', @@ -429,156 +430,166 @@ const ManageOrgConstantsComponent = ({ darkMode }) => { /> )} -
-
- {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/_styles/theme.scss b/frontend/src/_styles/theme.scss index 3c59855152..1b145788b4 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -15569,9 +15569,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/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/server/.version b/server/.version index e7bb755881..99ba0ddace 100644 --- a/server/.version +++ b/server/.version @@ -1 +1 @@ -3.0.1-ce-lts +3.0.2-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/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/services/app_import_export.service.ts b/server/src/services/app_import_export.service.ts index bcb95dfddd..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,13 +306,7 @@ 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)) { @@ -517,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); @@ -784,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); @@ -809,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); diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts index e63622b675..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'; @@ -435,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)) { @@ -457,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)) { @@ -660,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/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)) {