Merge pull request #11336 from ToolJet/release/v3.0.2-lts

Release v3.0.2 lts
This commit is contained in:
Johnson Cherian 2024-11-18 19:07:34 +05:30 committed by GitHub
commit 995a73254c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 650 additions and 297 deletions

View file

@ -1 +1 @@
3.0.1-ce-lts
3.0.2-ce-lts

View file

@ -1 +1 @@
3.0.1-ce-lts
3.0.2-ce-lts

View file

@ -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);
}

View file

@ -201,8 +201,9 @@ export const CreateVersion = ({ showCreateAppVersion, setShowCreateAppVersion })
width: '100%',
}}
>
{/* EE - change to development */}
<div className="" data-cy="workspace-constant-helper-text">
The new version will be created in development environment
The new version will be created in production environment
</div>
</div>
</Alert>

View file

@ -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');

View file

@ -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;

View file

@ -50,6 +50,7 @@ const RenderSchema = ({ component, parent, id, onOptionChange, onOptionsChange,
darkMode={darkMode}
fireEvent={fireEvent}
formId={formId}
id={id}
/>
);
};

View file

@ -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;
}

View file

@ -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;

View file

@ -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) {

View file

@ -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 }) => {
/>
</Drawer>
)}
<div className="align-items-center d-flex justify-content-between" style={{ marginBottom: '10px' }}>
<div className="tj-text-sm font-weight-500" data-cy="env-name">
{capitalize(activeTabEnvironment?.name)} ({globalCount + secretCount})
</div>
<div className="workspace-setting-buttons-wrap">
{canCreateVariable() && (
<ButtonSolid
data-cy="add-new-constant-button"
variant="primary"
onClick={() => {
setMode(() => MODES.CREATE);
setIsManageVarDrawerOpen(() => true);
}}
className="add-new-constant-button"
customStyles={{ minWidth: '200px', height: '32px' }}
disabled={isManageVarDrawerOpen}
>
+ Create new constant
</ButtonSolid>
)}
</div>
</div>
<div className="constant-page-wrapper">
<div className="container-xl">
<div>
<div className="workspace-constant-header">
<div className="tabs-and-search">
<div className="tabs">
<button
className={`tab ${activeTab === Constants.Global ? 'active' : ''}`}
onClick={() => handleTabChange(Constants.Global)}
>
Global constants
<span className={`tab-count ${activeTab === Constants.Global ? 'active' : ''}`}>
({globalCount})
</span>
</button>
<button
className={`tab ${activeTab === Constants.Secret ? 'active' : ''}`}
onClick={() => handleTabChange(Constants.Secret)}
>
Secrets
<span className={`tab-count ${activeTab === Constants.Secret ? 'active' : ''}`}>
({secretCount})
</span>
</button>
</div>
<div className="search-bar">
<SearchBox
width={250}
callBack={handleSearchChange}
customClass="tj-common-search-input-group"
autoFocus={true}
placeholder={activeTab === Constants.Global ? 'Search global constants' : 'Search secrets'}
onClearCallback={handleSearchClear}
/>
</div>
<div className="row gx-0">
<div className="organization-page-sidebar col">
<div className="workspace-nav-list-wrap">
<ManageOrgConstantsComponent.EnvironmentsTabs
allEnvironments={environments}
currentEnvironment={activeTabEnvironment}
setActiveTabEnvironment={setActiveTabEnvironment}
isLoading={isLoading}
allConstants={constants}
/>
</div>
<OrganizationList />
</div>
<div className="page-wrapper mt-4">
<div className="container-xl" style={{ width: '880px' }}>
<div className="align-items-center d-flex justify-content-between">
<div className="tj-text-sm font-weight-500" data-cy="env-name">
{capitalize(activeTabEnvironment?.name)} ({globalCount + secretCount})
</div>
<div className="workspace-setting-buttons-wrap">
{canCreateVariable() && (
<ButtonSolid
data-cy="add-new-constant-button"
variant="primary"
onClick={() => {
setMode(() => MODES.CREATE);
setIsManageVarDrawerOpen(() => true);
}}
className="add-new-constant-button"
customStyles={{ minWidth: '200px', height: '32px' }}
disabled={isManageVarDrawerOpen}
>
+ Create new constant
</ButtonSolid>
)}
</div>
</div>
</div>
</div>
<div className="workspace-variable-container-wrap mt-2">
<div className="container-xl" style={{ width: '880px', padding: '0px' }}>
<div className="workspace-constant-table-card">
<div className="mt-3">
<Alert svg="tj-info">
<div
className="d-flex align-items-center"
style={{
justifyContent: 'space-between',
flexWrap: 'wrap',
width: '100%',
}}
>
<div className="text-muted" data-cy="workspace-constant-helper-text">
{activeTab === Constants.Global ? (
<>
To resolve a global workspace constant use{' '}
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>{'{{constants.access_token}}'}</strong>
</>
) : (
<>
To resolve a secret workspace constant use{' '}
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>{'{{secrets.access_token}}'}</strong>
</>
)}
</div>
<div>
<Button
// Todo: Update link to documentation: workspace constants
onClick={() =>
window.open(
'https://docs.tooljet.com/docs/org-management/workspaces/workspace_constants/',
'_blank'
)
}
darkMode={darkMode}
size="sm"
styles={{
width: '100%',
fontSize: '12px',
fontWeight: 500,
}}
>
<Button.Content title={'Read documentation'} iconSrc="assets/images/icons/student.svg" />
</Button>
</div>
<div className="workspace-variable-container-wrap constants-list mt-2">
<div className="container-xl constant-page-wrapper">
<div className="workspace-constant-header">
<div className="tabs-and-search">
<div className="tabs">
<button
className={`tab ${activeTab === Constants.Global ? 'active' : ''}`}
onClick={() => handleTabChange(Constants.Global)}
>
Global constants
<span className={`tab-count ${activeTab === Constants.Global ? 'active' : ''}`}>
({globalCount})
</span>
</button>
<button
className={`tab ${activeTab === Constants.Secret ? 'active' : ''}`}
onClick={() => handleTabChange(Constants.Secret)}
>
Secrets
<span className={`tab-count ${activeTab === Constants.Secret ? 'active' : ''}`}>
({secretCount})
</span>
</button>
</div>
</Alert>
</div>
<div className="manage-sso-container h-100">
<div className="d-flex manage-constant-wrapper-card">
<ManageOrgConstantsComponent.EnvironmentsTabs
allEnvironments={environments}
currentEnvironment={activeTabEnvironment}
setActiveTabEnvironment={setActiveTabEnvironment}
isLoading={isLoading}
allConstants={constants}
/>
{(activeTab === Constants.Global && globalCount > 0) ||
(activeTab === Constants.Secret && secretCount > 0) ? (
<div className="w-100">
<ConstantTable
constants={currentTableData}
onEditBtnClicked={onEditBtnClicked}
onDeleteBtnClicked={onDeleteBtnClicked}
isLoading={isLoading}
canUpdateDeleteConstant={canUpdateVariable() || canDeleteVariable()}
/>
<ManageOrgConstantsComponent.Footer
darkMode={darkMode}
totalPage={totalPages}
pageCount={currentPage}
dataLoading={false}
gotoNextPage={goToNextPage}
gotoPreviousPage={goToPreviousPage}
showPagination={constants.length > 0}
/>
</div>
) : (
<EmptyState
canCreateVariable={canCreateVariable()}
setIsManageVarDrawerOpen={setIsManageVarDrawerOpen}
isLoading={isLoading}
searchTerm={searchTerm}
<div className="search-bar">
<SearchBox
width={250}
callBack={handleSearchChange}
customClass="tj-common-search-input-group"
autoFocus={true}
placeholder={activeTab === Constants.Global ? 'Search global constants' : 'Search secrets'}
onClearCallback={handleSearchClear}
/>
)}
</div>
</div>
</div>
<div className="workspace-constant-table-card">
<div className="mt-3">
<Alert svg="tj-info">
<div
className="d-flex align-items-center"
style={{
justifyContent: 'space-between',
flexWrap: 'wrap',
width: '100%',
}}
>
<div className="text-muted" data-cy="workspace-constant-helper-text">
{activeTab === Constants.Global ? (
<>
To resolve a global workspace constant use{' '}
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>
{'{{constants.access_token}}'}
</strong>
</>
) : (
<>
To resolve a secret workspace constant use{' '}
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>{'{{secrets.access_token}}'}</strong>
</>
)}
</div>
<div>
<Button
onClick={() =>
window.open(
'https://docs.tooljet.com/docs/org-management/workspaces/workspace_constants/',
'_blank'
)
}
darkMode={darkMode}
size="sm"
styles={{
width: '100%',
fontSize: '12px',
fontWeight: 500,
}}
>
<Button.Content title={'Read documentation'} iconSrc="assets/images/icons/student.svg" />
</Button>
</div>
</div>
</Alert>
</div>
<div className="manage-sso-container h-100">
<div className="d-flex manage-constant-wrapper-card">
{(activeTab === Constants.Global && globalCount > 0) ||
(activeTab === Constants.Secret && secretCount > 0) ? (
<div className="w-100">
<ConstantTable
constants={currentTableData}
onEditBtnClicked={onEditBtnClicked}
onDeleteBtnClicked={onDeleteBtnClicked}
isLoading={isLoading}
canUpdateDeleteConstant={canUpdateVariable() || canDeleteVariable()}
/>
<ManageOrgConstantsComponent.Footer
darkMode={darkMode}
totalPage={totalPages}
pageCount={currentPage}
dataLoading={false}
gotoNextPage={goToNextPage}
gotoPreviousPage={goToPreviousPage}
showPagination={constants.length > 0}
/>
</div>
) : (
<EmptyState
canCreateVariable={canCreateVariable()}
setIsManageVarDrawerOpen={setIsManageVarDrawerOpen}
isLoading={isLoading}
searchTerm={searchTerm}
/>
)}
</div>
</div>
</div>
</div>

View file

@ -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 {

View file

@ -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();
});
}
};

View file

@ -118,9 +118,11 @@ const SignupForm = ({
onSubmit(
formData,
() => {
// Success callback
setIsLoading(false);
},
() => {
// Error callback
setIsLoading(false);
}
);

View file

@ -1 +1 @@
3.0.1-ce-lts
3.0.2-ce-lts

View file

@ -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",

View file

@ -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) {

View file

@ -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<string, string> = {}) {
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<string, str
return node;
}
function containsBracketNotation(queryString) {
const bracketNotationRegex = /\[\s*['"][^'"]+['"]\s*\]/;
return bracketNotationRegex.test(queryString);
}
export function findAllEntityReferences(node, allRefs): [] {
if (typeof node === 'object') {
for (const key in node) {
const value = node[key];
if (typeof value === 'string' && containsBracketNotation(value)) {
//skip if the value is a bracket notation
break;
}
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
const referenceExists = value;
if (referenceExists) {
const matches = value.match(/{{(.*?)}}/g);
if (matches) {
matches.forEach((match) => {
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 };
}

View file

@ -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<string, string>;
dataQueryMapping: Record<string, string>;
@ -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);

View file

@ -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;

View file

@ -347,7 +347,7 @@ export class AuthService {
return dbTransactionWrap(async (manager: EntityManager) => {
// Check if the configs allows user signups
if (this.configService.get<string>('DISABLE_SIGNUPS') === 'true') {
if (!organizationId && this.configService.get<string>('DISABLE_SIGNUPS') === 'true') {
throw new NotAcceptableException();
}

View file

@ -146,7 +146,7 @@ export class OrganizationsService {
enable_sign_up: this.configService.get<string>('DISABLE_SIGNUPS') !== 'true',
enabled: true,
},
enableSignUp: this.configService.get<string>('SSO_DISABLE_SIGNUPS') !== 'true',
enableSignUp: this.configService.get<string>('DISABLE_SIGNUPS') !== 'true',
};
}

View file

@ -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)) {