mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 00:48:25 +00:00
Merge branch 'main' into release/marketplace-sprint-11
This commit is contained in:
commit
5e53d6dd7e
102 changed files with 2965 additions and 941 deletions
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
3.12.0
|
||||
3.13.0
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.12.0
|
||||
3.13.0
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 518f3334b12a83785fd37dd53b0245d72848211a
|
||||
Subproject commit 777446d71e78e5941d34353606a12d982820438f
|
||||
|
|
@ -58,6 +58,7 @@
|
|||
"dotenv": "^16.0.3",
|
||||
"draft-js": "^0.11.7",
|
||||
"draft-js-export-html": "^1.4.1",
|
||||
"draft-js-import-html": "^1.4.1",
|
||||
"driver.js": "^0.9.8",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"file-loader": "^6.2.0",
|
||||
|
|
|
|||
|
|
@ -232,6 +232,7 @@ export const getAllChildComponents = (allComponents, parentId) => {
|
|||
const childTabId = componentParentId.split('-').at(-1);
|
||||
if (componentParentId === `${parentId}-${childTabId}`) {
|
||||
childComponent.isParentTabORCalendar = true;
|
||||
childComponent.events = useStore.getState().eventsSlice.getEventsByComponentsId(componentId);
|
||||
childComponents.push(childComponent);
|
||||
// Recursively find children of the current child component
|
||||
const childrenOfChild = getAllChildComponents(allComponents, componentId);
|
||||
|
|
@ -242,6 +243,7 @@ export const getAllChildComponents = (allComponents, parentId) => {
|
|||
if (componentParentId === parentId) {
|
||||
let childComponent = deepClone(allComponents[componentId]);
|
||||
childComponent.id = componentId;
|
||||
childComponent.events = useStore.getState().eventsSlice.getEventsByComponentsId(componentId);
|
||||
childComponents.push(childComponent);
|
||||
|
||||
// Recursively find children of the current child component
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/* eslint-disable import/no-unresolved */
|
||||
import React from 'react';
|
||||
import { openSearchPanel } from '@codemirror/search';
|
||||
import './SearchBox.scss';
|
||||
import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx';
|
||||
|
||||
export const CodeHinterBtns = ({ view, isPanelOpen, renderCopilot }) => {
|
||||
return (
|
||||
<div
|
||||
className="d-flex tw-flex-col align-items-end tw-gap-[2.5px] w-100 position-absolute tw-pt-[4px] tw-pr-[4px]"
|
||||
style={{ top: isPanelOpen ? '44px' : 0 }}
|
||||
>
|
||||
{!isPanelOpen && (
|
||||
<ButtonComponent
|
||||
iconOnly
|
||||
trailingIcon="search01"
|
||||
size="small"
|
||||
variant="outline"
|
||||
ariaLabel="Open search panel"
|
||||
className="codehinter-search-btn"
|
||||
onClick={() => openSearchPanel(view)}
|
||||
/>
|
||||
)}
|
||||
{renderCopilot && renderCopilot()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -20,10 +20,12 @@ import { PreviewBox } from './PreviewBox';
|
|||
import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import { search, searchKeymap, searchPanelOpen } from '@codemirror/search';
|
||||
import { handleSearchPanel, SearchBtn } from './SearchBox';
|
||||
import { handleSearchPanel } from './SearchBox';
|
||||
import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks';
|
||||
import { isInsideParent } from './utils';
|
||||
import { CodeHinterBtns } from './CodehinterOverlayTriggers';
|
||||
|
||||
const langSupport = Object.freeze({
|
||||
javascript: javascript(),
|
||||
|
|
@ -66,7 +68,7 @@ const MultiLineCodeEditor = (props) => {
|
|||
|
||||
const context = useContext(CodeHinterContext);
|
||||
|
||||
const { suggestionList } = createReferencesLookup(context, true);
|
||||
const { suggestionList: paramList } = createReferencesLookup(context, true);
|
||||
|
||||
const currentValueRef = useRef(initialValue);
|
||||
|
||||
|
|
@ -74,6 +76,7 @@ const MultiLineCodeEditor = (props) => {
|
|||
|
||||
const [editorView, setEditorView] = React.useState(null);
|
||||
|
||||
const [isSearchPanelOpen, setIsSearchPanelOpen] = React.useState(false);
|
||||
const { queryPanelKeybindings } = useQueryPanelKeyHooks(onChange, currentValueRef, 'multiline');
|
||||
|
||||
const handleOnBlur = () => {
|
||||
|
|
@ -146,8 +149,29 @@ const MultiLineCodeEditor = (props) => {
|
|||
return suggestion.hint.includes(nearestSubstring);
|
||||
});
|
||||
|
||||
const localVariables = new Set();
|
||||
|
||||
// Traverse the syntax tree to extract variable declarations
|
||||
syntaxTree(context.state).iterate({
|
||||
enter: (node) => {
|
||||
// JavaScript: Detect variable declarations (var, let, const)
|
||||
if (node.name === 'VariableDefinition') {
|
||||
const varName = context.state.sliceDoc(node.from, node.to);
|
||||
if (varName && varName.startsWith(nearestSubstring)) localVariables.add(varName);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Convert Set to an array of completion suggestions
|
||||
const localVariableSuggestions = [...localVariables].map((varName) => ({
|
||||
hint: varName,
|
||||
type: 'variable',
|
||||
}));
|
||||
|
||||
const suggestionList = paramList.filter((paramSuggestion) => paramSuggestion.hint.includes(nearestSubstring));
|
||||
|
||||
const suggestions = generateHints(
|
||||
[...JSLangHints, ...autoSuggestionList, ...suggestionList],
|
||||
[...localVariableSuggestions, ...JSLangHints, ...autoSuggestionList, ...suggestionList],
|
||||
null,
|
||||
nearestSubstring
|
||||
).map((hint) => {
|
||||
|
|
@ -204,6 +228,7 @@ const MultiLineCodeEditor = (props) => {
|
|||
return {
|
||||
from: context.pos,
|
||||
options: [...suggestions],
|
||||
filter: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -237,7 +262,7 @@ const MultiLineCodeEditor = (props) => {
|
|||
]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), []);
|
||||
const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [paramList]);
|
||||
const { handleTogglePopupExapand, isOpen, setIsOpen, forceUpdate } = portalProps;
|
||||
let cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : props.cyLabel;
|
||||
|
||||
|
|
@ -258,7 +283,7 @@ const MultiLineCodeEditor = (props) => {
|
|||
ref={wrapperRef}
|
||||
>
|
||||
<div className={`${className} ${darkMode && 'cm-codehinter-dark-themed'}`}>
|
||||
<SearchBtn view={editorView} />
|
||||
<CodeHinterBtns view={editorView} isPanelOpen={isSearchPanelOpen} renderCopilot={renderCopilot} />
|
||||
<CodeHinter.PopupIcon
|
||||
callback={handleTogglePopupExapand}
|
||||
icon="portal-open"
|
||||
|
|
@ -266,7 +291,6 @@ const MultiLineCodeEditor = (props) => {
|
|||
isMultiEditor={true}
|
||||
isQueryManager={isInsideQueryPane}
|
||||
/>
|
||||
{renderCopilot && renderCopilot()}
|
||||
|
||||
<CodeHinter.Portal
|
||||
isCopilotEnabled={false}
|
||||
|
|
@ -326,12 +350,7 @@ const MultiLineCodeEditor = (props) => {
|
|||
readOnly={readOnly}
|
||||
editable={editable} //for transformations in query manager
|
||||
onCreateEditor={(view) => setEditorView(view)}
|
||||
onUpdate={(view) => {
|
||||
const icon = document.querySelector('.codehinter-search-btn');
|
||||
if (searchPanelOpen(view.state)) {
|
||||
icon.style.display = 'none';
|
||||
} else icon.style.display = 'block';
|
||||
}}
|
||||
onUpdate={(view) => setIsSearchPanelOpen(searchPanelOpen(view.state))}
|
||||
/>
|
||||
</div>
|
||||
{showPreview && (
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
findPrevious,
|
||||
replaceNext,
|
||||
replaceAll,
|
||||
openSearchPanel,
|
||||
} from '@codemirror/search';
|
||||
import './SearchBox.scss';
|
||||
import InputComponent from '@/components/ui/Input/Index.jsx';
|
||||
|
|
@ -162,22 +161,3 @@ function SearchPanel({ view }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const SearchBtn = ({ view }) => {
|
||||
return (
|
||||
<div
|
||||
className="d-flex justify-content-end w-100 position-absolute tw-pt-[3px] tw-pr-[4px] codehinter-search-btn-wrapper"
|
||||
style={{ top: 0 }}
|
||||
>
|
||||
<ButtonComponent
|
||||
iconOnly
|
||||
trailingIcon="search01"
|
||||
size="small"
|
||||
variant="outline"
|
||||
ariaLabel="Open search panel"
|
||||
className="codehinter-search-btn"
|
||||
onClick={() => openSearchPanel(view)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,7 +44,5 @@
|
|||
}
|
||||
|
||||
.code-hinter-wrapper .codehinter-search-btn {
|
||||
display: block;
|
||||
padding-top: 1px;
|
||||
z-index: 10000;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
|
@ -1,12 +1,18 @@
|
|||
/* eslint-disable import/no-unresolved */
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState, useContext } from 'react';
|
||||
import { PreviewBox } from './PreviewBox';
|
||||
import { ToolTip } from '@/Editor/Inspector/Elements/Components/ToolTip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { camelCase, isEmpty, noop, get } from 'lodash';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { autocompletion, completionKeymap, completionStatus, acceptCompletion } from '@codemirror/autocomplete';
|
||||
import {
|
||||
autocompletion,
|
||||
completionKeymap,
|
||||
completionStatus,
|
||||
acceptCompletion,
|
||||
startCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { keymap } from '@codemirror/view';
|
||||
import FxButton from '../CodeBuilder/Elements/FxButton';
|
||||
|
|
@ -22,6 +28,8 @@ import CodeHinter from './CodeHinter';
|
|||
import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { CodeHinterContext } from '../CodeBuilder/CodeHinterContext';
|
||||
import { createReferencesLookup } from '@/_stores/utils';
|
||||
import { useQueryPanelKeyHooks } from './useQueryPanelKeyHooks';
|
||||
|
||||
const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => {
|
||||
|
|
@ -73,6 +81,7 @@ const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...r
|
|||
if (typeof initialValue === 'string' && (initialValue?.includes('components') || initialValue?.includes('queries'))) {
|
||||
newInitialValue = replaceIdsWithName(initialValue);
|
||||
}
|
||||
|
||||
//! Re render the component when the componentName changes as the initialValue is not updated
|
||||
|
||||
// const { variablesExposedForPreview } = useContext(EditorContext) || {};
|
||||
|
|
@ -199,9 +208,14 @@ const EditorInput = ({
|
|||
wrapperRef,
|
||||
showSuggestions,
|
||||
}) => {
|
||||
const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow);
|
||||
const codeHinterContext = useContext(CodeHinterContext);
|
||||
const { suggestionList: paramHints } = createReferencesLookup(codeHinterContext, true);
|
||||
|
||||
const getSuggestions = useStore((state) => state.getSuggestions, shallow);
|
||||
const [codeMirrorView, setCodeMirrorView] = useState(undefined);
|
||||
|
||||
const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow);
|
||||
|
||||
const { queryPanelKeybindings } = useQueryPanelKeyHooks(onBlurUpdate, currentValue, 'singleline');
|
||||
|
||||
const isInsideQueryManager = useMemo(
|
||||
|
|
@ -209,16 +223,16 @@ const EditorInput = ({
|
|||
[wrapperRef.current]
|
||||
);
|
||||
function autoCompleteExtensionConfig(context) {
|
||||
const hints = getSuggestions();
|
||||
const hintsWithoutParamHints = getSuggestions();
|
||||
const serverHints = getServerSideGlobalSuggestions(isInsideQueryManager);
|
||||
|
||||
const allHints = {
|
||||
...hints,
|
||||
appHints: [...hints.appHints, ...serverHints],
|
||||
};
|
||||
|
||||
let word = context.matchBefore(/\w*/);
|
||||
|
||||
const hints = {
|
||||
...hintsWithoutParamHints,
|
||||
appHints: [...hintsWithoutParamHints.appHints, ...serverHints, ...paramHints],
|
||||
};
|
||||
|
||||
const totalReferences = (context.state.doc.toString().match(/{{/g) || []).length;
|
||||
|
||||
let queryInput = context.state.doc.toString();
|
||||
|
|
@ -247,17 +261,18 @@ const EditorInput = ({
|
|||
queryInput = '{{' + currentWord + '}}';
|
||||
}
|
||||
|
||||
let completions = getAutocompletion(queryInput, validationType, allHints, totalReferences, originalQueryInput);
|
||||
let completions = getAutocompletion(queryInput, validationType, hints, totalReferences, originalQueryInput);
|
||||
|
||||
return {
|
||||
from: word.from,
|
||||
options: completions,
|
||||
validFor: /^\{\{.*\}\}$/,
|
||||
filter: false,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [isInsideQueryManager]);
|
||||
const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [isInsideQueryManager, paramHints]);
|
||||
|
||||
const autoCompleteConfig = autocompletion({
|
||||
override: [overRideFunction],
|
||||
|
|
@ -424,6 +439,9 @@ const EditorInput = ({
|
|||
ref={previewRef}
|
||||
>
|
||||
<CodeMirror
|
||||
onCreateEditor={(view) => {
|
||||
setCodeMirrorView(view);
|
||||
}}
|
||||
value={currentValue}
|
||||
placeholder={placeholder}
|
||||
height={isInsideQueryPane ? '100%' : showLineNumbers ? '400px' : '100%'}
|
||||
|
|
@ -460,11 +478,16 @@ const EditorInput = ({
|
|||
theme={theme}
|
||||
indentWithTab={false}
|
||||
readOnly={disabled}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Backspace') {
|
||||
startCompletion(codeMirrorView);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</CodeHinter.Portal>
|
||||
</div>
|
||||
</ErrorBoundary >
|
||||
</CodeHinter.Portal >
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@ export const getAutocompletion = (input, fieldType, hints, totalReferences = 1,
|
|||
originalQueryInput,
|
||||
searchInput
|
||||
);
|
||||
return orderSuggestions(suggestions, fieldType);
|
||||
|
||||
return suggestions;
|
||||
};
|
||||
|
||||
function orderSuggestions(suggestions, validationType) {
|
||||
|
|
@ -90,10 +91,18 @@ export const generateHints = (hints, totalReferences = 1, input, searchText) =>
|
|||
const hasDepth = currentWord.includes('.');
|
||||
const lastDepth = getLastSubstring(currentWord);
|
||||
|
||||
const displayLabel = getLastDepth(displayedHint);
|
||||
let displayLabel = getLastDepth(displayedHint);
|
||||
|
||||
if (type != 'js_method') {
|
||||
const currentWordDepth = currentWord.split('.').length;
|
||||
displayLabel = hint
|
||||
.split('.')
|
||||
.slice(currentWordDepth - 1)
|
||||
.join('.');
|
||||
}
|
||||
|
||||
return {
|
||||
displayLabel: lastDepth === '' ? displayedHint : displayLabel,
|
||||
displayLabel,
|
||||
label: displayedHint,
|
||||
info: displayedHint,
|
||||
type: type === 'js_method' ? 'js_methods' : type?.toLowerCase(),
|
||||
|
|
@ -154,40 +163,24 @@ export const generateHints = (hints, totalReferences = 1, input, searchText) =>
|
|||
};
|
||||
|
||||
function filterHintsByDepth(input, hints) {
|
||||
if (input === '') return hints;
|
||||
const inputParts = input.split('.');
|
||||
const inputDepth = inputParts.length + 1;
|
||||
|
||||
const inputDepth = input.includes('.') ? input.split('.').length : 0;
|
||||
|
||||
const filteredHints = hints.filter((cm) => {
|
||||
const hintParts = cm.hint.split('.');
|
||||
|
||||
let shouldInclude =
|
||||
(cm.hint.startsWith(input) && hintParts.length === inputDepth + 1) ||
|
||||
(cm.hint.startsWith(input) && hintParts.length === inputDepth);
|
||||
|
||||
const shouldFuzzyMatch = !shouldInclude ? hintParts.length > inputDepth : false;
|
||||
|
||||
if (shouldFuzzyMatch) {
|
||||
// fuzzy match
|
||||
let matchedDepth = -1;
|
||||
for (let i = 0; i < hintParts.length; i++) {
|
||||
if (hintParts[i].includes(input)) {
|
||||
matchedDepth = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedDepth !== -1) {
|
||||
shouldInclude = hintParts.length === matchedDepth + 1;
|
||||
}
|
||||
} else if (input.endsWith('.')) {
|
||||
shouldInclude = cm.hint.startsWith(input) && hintParts.length === inputDepth;
|
||||
}
|
||||
|
||||
return shouldInclude;
|
||||
const hintsWithDepth = hints.map((hint) => {
|
||||
const hintParts = hint.hint.split('.');
|
||||
return {
|
||||
...hint,
|
||||
depth: hintParts.length,
|
||||
};
|
||||
});
|
||||
|
||||
return filteredHints;
|
||||
const filteredHints = hintsWithDepth.filter((hint) => {
|
||||
return hint.depth <= inputDepth;
|
||||
});
|
||||
|
||||
const sortedHints = filteredHints.sort((hint1, hint2) => hint1.depth - hint2.depth);
|
||||
|
||||
return sortedHints;
|
||||
}
|
||||
|
||||
export function findNearestSubstring(inputStr, currentCurosorPos) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,20 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Select from '@/_ui/Select';
|
||||
import { decodeEntities } from '@/_helpers/utils';
|
||||
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
|
||||
|
||||
export const ChangeDataSource = ({ dataSources, onChange, value, isVersionReleased }) => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
usePopoverObserver(
|
||||
document.getElementsByClassName('query-details')[0],
|
||||
document.querySelector('.change-data-source-select.react-select__control'),
|
||||
document.querySelector('.change-data-source-select.react-select__menu'),
|
||||
isMenuOpen,
|
||||
() => (document.querySelector('.change-data-source-select.react-select__menu').style.display = 'block'),
|
||||
() => (document.querySelector('.change-data-source-select.react-select__menu').style.display = 'none')
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
className="w-100"
|
||||
|
|
@ -14,6 +26,13 @@ export const ChangeDataSource = ({ dataSources, onChange, value, isVersionReleas
|
|||
}}
|
||||
useMenuPortal={true}
|
||||
isDisabled={isVersionReleased}
|
||||
customClassPrefix="change-data-source-select"
|
||||
onMenuOpen={() => {
|
||||
setIsMenuOpen(true);
|
||||
}}
|
||||
onMenuClose={() => {
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ class Restapi extends React.Component {
|
|||
codeHinterHeight: 32, // Default height
|
||||
};
|
||||
this.codeHinterRef = React.createRef();
|
||||
this.isMenuOpenRef = React.createRef();
|
||||
this.prevIsMenuOpenRef = React.createRef(false);
|
||||
this.intersectionObserver = null;
|
||||
this.resizeObserver = null;
|
||||
}
|
||||
|
||||
|
|
@ -47,6 +50,9 @@ class Restapi extends React.Component {
|
|||
if (this.codeHinterRef.current && !this.resizeObserver) {
|
||||
this.setupResizeObserver();
|
||||
}
|
||||
if (!this.intersectionObserver) {
|
||||
this.setupIntersectionObserver();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
|
@ -75,6 +81,7 @@ class Restapi extends React.Component {
|
|||
}, 1000);
|
||||
|
||||
this.setupResizeObserver();
|
||||
this.setupIntersectionObserver();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
|
@ -84,6 +91,9 @@ class Restapi extends React.Component {
|
|||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
if (this.intersectionObserver) {
|
||||
this.intersectionObserver.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
setupResizeObserver() {
|
||||
|
|
@ -132,6 +142,33 @@ class Restapi extends React.Component {
|
|||
this.resizeObserver.observe(element);
|
||||
}
|
||||
|
||||
setupIntersectionObserver() {
|
||||
const container = document.getElementsByClassName('query-details')[0];
|
||||
const trigger = document.querySelector('.restapi-method-select.react-select__control');
|
||||
|
||||
if (this.intersectionObserver) {
|
||||
this.intersectionObserver.disconnect();
|
||||
}
|
||||
|
||||
this.intersectionObserver = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
const popover = document.querySelector('.restapi-method-select.react-select__menu');
|
||||
if (entry.isIntersecting) {
|
||||
if (this.prevIsMenuOpenRef.current) {
|
||||
popover.style.display = 'block';
|
||||
this.prevIsMenuOpenRef.current = false;
|
||||
}
|
||||
} else if (this.isMenuOpenRef.current) {
|
||||
popover.style.display = 'none';
|
||||
this.prevIsMenuOpenRef.current = true;
|
||||
}
|
||||
},
|
||||
{ root: container, threshold: [0.5] }
|
||||
);
|
||||
|
||||
this.intersectionObserver.observe(trigger);
|
||||
}
|
||||
|
||||
initizalizeRetryNetworkErrorsToggle = () => {
|
||||
const isRetryNetworkErrorToggleUnused = this.props.options.retry_network_errors === null;
|
||||
if (isRetryNetworkErrorToggleUnused) {
|
||||
|
|
@ -287,6 +324,13 @@ class Restapi extends React.Component {
|
|||
height={32}
|
||||
styles={this.customSelectStyles(this.props.darkMode, 91)}
|
||||
useCustomStyles={true}
|
||||
customClassPrefix="restapi-method-select"
|
||||
onMenuOpen={() => {
|
||||
this.isMenuOpenRef.current = true;
|
||||
}}
|
||||
onMenuClose={() => {
|
||||
this.isMenuOpenRef.current = false;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -5,14 +5,25 @@ import CodeHinter from '@/AppBuilder/CodeEditor';
|
|||
import './workflows-query.scss';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
|
||||
|
||||
export function Workflows({ options, optionsChanged, currentState }) {
|
||||
const [workflowOptions, setWorkflowOptions] = useState([]);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [_selectedWorkflowId, setSelectedWorkflowId] = useState(undefined);
|
||||
const [params, setParams] = useState([...(options.params ?? [{ key: '', value: '' }])]);
|
||||
|
||||
const appId = useStore((state) => state.app.appId);
|
||||
|
||||
usePopoverObserver(
|
||||
document.getElementsByClassName('query-details')[0],
|
||||
document.querySelector('.workflow-select.react-select__control'),
|
||||
document.querySelector('.workflow-select.react-select__menu'),
|
||||
isMenuOpen,
|
||||
() => (document.querySelector('.workflow-select.react-select__menu').style.display = 'block'),
|
||||
() => (document.querySelector('.workflow-select.react-select__menu').style.display = 'none')
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
appsService.getWorkflows(appId).then(({ workflows }) => {
|
||||
setWorkflowOptions(
|
||||
|
|
@ -50,6 +61,13 @@ export function Workflows({ options, optionsChanged, currentState }) {
|
|||
customWrap={true}
|
||||
width="300px"
|
||||
menuPlacement="bottom"
|
||||
customClassPrefix="workflow-select"
|
||||
onMenuOpen={() => {
|
||||
setIsMenuOpen(true);
|
||||
}}
|
||||
onMenuClose={() => {
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
<label className="my-2">Params</label>
|
||||
<div className="grid"></div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,538 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import Accordion from '@/_ui/Accordion';
|
||||
import { EventManager } from '../EventManager';
|
||||
import { renderElement } from '../Utils';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import Popover from 'react-bootstrap/Popover';
|
||||
import List from '@/ToolJetUI/List/List';
|
||||
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import CodeHinter from '@/AppBuilder/CodeEditor';
|
||||
import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton';
|
||||
import ListGroup from 'react-bootstrap/ListGroup';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import SortableList from '@/_components/SortableList';
|
||||
import Trash from '@/_ui/Icon/solidIcons/Trash';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import Switch from '@/Editor/CodeBuilder/Elements/Switch';
|
||||
import { usePrevious } from '@dnd-kit/utilities';
|
||||
|
||||
export function Steps({ componentMeta, darkMode, ...restProps }) {
|
||||
const {
|
||||
layoutPropertyChanged,
|
||||
component,
|
||||
dataQueries,
|
||||
paramUpdated,
|
||||
currentState,
|
||||
eventsChanged,
|
||||
apps,
|
||||
allComponents,
|
||||
pages,
|
||||
} = restProps;
|
||||
const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
|
||||
|
||||
const isDynamicOptionsEnabled = getResolvedValue(component?.component?.definition?.properties?.advanced?.value);
|
||||
const variant = component?.component?.definition?.properties?.variant?.value;
|
||||
const prevVariant = usePrevious(variant)
|
||||
console.log("variant", component?.component?.definition);
|
||||
|
||||
|
||||
const [options, setOptions] = useState([]);
|
||||
const [hoveredOptionIndex, setHoveredOptionIndex] = useState(null);
|
||||
let properties = [];
|
||||
let additionalActions = [];
|
||||
let optionsProperties = [];
|
||||
|
||||
for (const [key] of Object.entries(componentMeta?.properties)) {
|
||||
if (componentMeta?.properties[key]?.section === 'additionalActions') {
|
||||
additionalActions.push(key);
|
||||
} else if (componentMeta?.properties[key]?.accordian === 'Options') {
|
||||
optionsProperties.push(key);
|
||||
} else {
|
||||
properties.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// the default style of "number" & "titles" type are different for completed label
|
||||
// TODO: Need to revisit this logic when text custom themes are implemented
|
||||
useEffect(() => {
|
||||
const completedLabelColor = component?.component?.definition?.styles?.completedLabel?.value;
|
||||
if (variant !== prevVariant) {
|
||||
if (variant === "numbers" && completedLabelColor === "#1B1F24") {
|
||||
paramUpdated({ name: 'completedLabel' }, 'value', "#FFFFFF", 'styles', false, {});
|
||||
} else if (variant === "titles" && completedLabelColor === "#FFFFFF") {
|
||||
paramUpdated({ name: 'completedLabel' }, 'value', "#1B1F24", 'styles', false, {});
|
||||
}
|
||||
}
|
||||
|
||||
}, [variant])
|
||||
|
||||
const getItemStyle = (isDragging, draggableStyle) => ({
|
||||
userSelect: 'none',
|
||||
...draggableStyle,
|
||||
});
|
||||
|
||||
const updateAllOptionsParams = (options, props) => {
|
||||
paramUpdated({ name: 'steps' }, 'value', options, 'properties', false, props);
|
||||
};
|
||||
|
||||
const generateNewOptions = () => {
|
||||
let found = false;
|
||||
let label = '';
|
||||
let currentNumber = options.length + 1;
|
||||
while (!found) {
|
||||
label = `step ${currentNumber}`;
|
||||
if (options.find((option) => option.name === label) === undefined) {
|
||||
found = true;
|
||||
}
|
||||
currentNumber += 1;
|
||||
}
|
||||
return {
|
||||
name: label,
|
||||
id: currentNumber - 1,
|
||||
tooltip: label,
|
||||
visible: { value: '{{true}}' },
|
||||
disabled: { value: '{{false}}' },
|
||||
};
|
||||
};
|
||||
|
||||
const handleAddOption = () => {
|
||||
let _option = generateNewOptions();
|
||||
const _items = [...options, _option];
|
||||
setOptions(_items);
|
||||
updateAllOptionsParams(_items);
|
||||
};
|
||||
const handleDeleteOption = (index) => {
|
||||
const _items = options.filter((option, i) => i !== index);
|
||||
setOptions(_items);
|
||||
updateAllOptionsParams(_items, { isParamFromDropdownOptions: true });
|
||||
};
|
||||
|
||||
const handleLabelChange = (propertyName, value, index) => {
|
||||
const _options = options.map((option, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...option,
|
||||
[propertyName]: value,
|
||||
};
|
||||
}
|
||||
return option;
|
||||
});
|
||||
setOptions(_options);
|
||||
updateAllOptionsParams(_options);
|
||||
};
|
||||
|
||||
const reorderOptions = async (startIndex, endIndex) => {
|
||||
const result = [...options];
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
setOptions(result);
|
||||
updateAllOptionsParams(result);
|
||||
};
|
||||
|
||||
const onDragEnd = ({ source, destination }) => {
|
||||
if (!destination || source?.index === destination?.index) {
|
||||
return;
|
||||
}
|
||||
reorderOptions(source.index, destination.index);
|
||||
};
|
||||
|
||||
const handleOnFxPress = (active, index, key) => {
|
||||
const _options = options.map((option, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...option,
|
||||
[key]: {
|
||||
...option[key],
|
||||
fxActive: active,
|
||||
},
|
||||
};
|
||||
}
|
||||
return option;
|
||||
});
|
||||
setOptions(_options);
|
||||
updateAllOptionsParams(_options);
|
||||
};
|
||||
|
||||
const _renderOverlay = (item, index) => {
|
||||
return (
|
||||
<Popover className={`${darkMode && 'dark-theme theme-dark'}`} style={{ minWidth: '248px' }}>
|
||||
<Popover.Body>
|
||||
<div className="field mb-3" data-cy={`input-and-label-column-name`}>
|
||||
<label data-cy={`label-column-name`} className="font-weight-500 mb-1 font-size-12">
|
||||
{'Id'}
|
||||
</label>
|
||||
<CodeHinter
|
||||
type={'basic'}
|
||||
initialValue={item?.id + ''}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
placeholder={'Option label'}
|
||||
onChange={(value) => handleLabelChange('id', value, index)}
|
||||
/>
|
||||
</div>
|
||||
<div className="field mb-3" data-cy={`input-and-label-column-name`}>
|
||||
<label data-cy={`label-column-name`} className="font-weight-500 mb-1 font-size-12">
|
||||
{'Label'}
|
||||
</label>
|
||||
<CodeHinter
|
||||
type={'basic'}
|
||||
initialValue={item?.name}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
placeholder={'Option label'}
|
||||
onChange={(value) => handleLabelChange('name', value, index)}
|
||||
/>
|
||||
</div>
|
||||
<div className="field mb-3" data-cy={`input-and-label-column-name`}>
|
||||
<label data-cy={`label-column-name`} className="font-weight-500 mb-1 font-size-12">
|
||||
{'Tooltip'}
|
||||
</label>
|
||||
<CodeHinter
|
||||
type={'basic'}
|
||||
initialValue={item?.tooltip + ''}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
placeholder={'Tooltip'}
|
||||
onChange={(value) => handleLabelChange('tooltip', value, index)}
|
||||
/>
|
||||
</div>
|
||||
<div className="field mb-2" data-cy={`input-and-label-column-name`}>
|
||||
<CodeHinter
|
||||
initialValue={item?.visible?.value}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
component={component}
|
||||
type={'fxEditor'}
|
||||
paramLabel={'Visibility'}
|
||||
onChange={(value) =>
|
||||
handleLabelChange(
|
||||
'visible',
|
||||
{
|
||||
value,
|
||||
},
|
||||
index
|
||||
)
|
||||
}
|
||||
paramName={'visible'}
|
||||
onFxPress={(active) => handleOnFxPress(active, index, 'visible')}
|
||||
fxActive={item?.visible?.fxActive}
|
||||
fieldMeta={{
|
||||
type: 'toggle',
|
||||
displayName: 'Make editable',
|
||||
}}
|
||||
paramType={'toggle'}
|
||||
/>
|
||||
</div>
|
||||
<div className="field" data-cy={`input-and-label-column-name`}>
|
||||
<CodeHinter
|
||||
initialValue={item?.disabled?.value}
|
||||
theme={darkMode ? 'monokai' : 'default'}
|
||||
mode="javascript"
|
||||
lineNumbers={false}
|
||||
component={component}
|
||||
type={'fxEditor'}
|
||||
paramLabel={'Disable'}
|
||||
paramName={'disable'}
|
||||
onChange={(value) => handleLabelChange('disabled', { value }, index)}
|
||||
onFxPress={(active) => handleOnFxPress(active, index, 'disabled')}
|
||||
fxActive={item?.disabled?.fxActive}
|
||||
fieldMeta={{
|
||||
type: 'toggle',
|
||||
displayName: 'Make editable',
|
||||
}}
|
||||
paramType={'toggle'}
|
||||
/>
|
||||
</div>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
const _renderOptions = () => {
|
||||
return (
|
||||
<List style={{ marginBottom: '20px' }}>
|
||||
<DragDropContext
|
||||
onDragEnd={(result) => {
|
||||
onDragEnd(result);
|
||||
}}
|
||||
>
|
||||
<Droppable droppableId="droppable">
|
||||
{({ innerRef, droppableProps, placeholder }) => (
|
||||
<div className="w-100" {...droppableProps} ref={innerRef}>
|
||||
{options?.map((item, index) => {
|
||||
return (
|
||||
<Draggable key={item.name} draggableId={item.name} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
key={index}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
|
||||
>
|
||||
<OverlayTrigger
|
||||
trigger="click"
|
||||
placement="left"
|
||||
rootClose
|
||||
overlay={_renderOverlay(item, index)}
|
||||
>
|
||||
<div key={item.name + item.id}>
|
||||
<ListGroup.Item
|
||||
style={{ marginBottom: '8px', backgroundColor: 'var(--slate3)' }}
|
||||
onMouseEnter={() => setHoveredOptionIndex(index)}
|
||||
onMouseLeave={() => setHoveredOptionIndex(null)}
|
||||
{...restProps}
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col-auto d-flex align-items-center">
|
||||
<SortableList.DragHandle show />
|
||||
</div>
|
||||
<div className="col text-truncate cursor-pointer" style={{ padding: '0px' }}>
|
||||
{getResolvedValue(item.name)}
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
{index === hoveredOptionIndex && (
|
||||
<ButtonSolid
|
||||
variant="danger"
|
||||
size="xs"
|
||||
className={'delete-icon-btn'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteOption(index);
|
||||
}}
|
||||
>
|
||||
<span className="d-flex">
|
||||
<Trash fill={'var(--tomato9)'} width={12} />
|
||||
</span>
|
||||
</ButtonSolid>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ListGroup.Item>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<AddNewButton onClick={handleAddOption} dataCy="add-new-dropdown-option" className="mt-0">
|
||||
Add new option
|
||||
</AddNewButton>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
const isDynamicStepsEnabled = getResolvedValue(component?.component?.definition?.properties?.advanced?.value);
|
||||
useEffect(() => {
|
||||
setOptions(constructSteps());
|
||||
}, [component?.id, isDynamicStepsEnabled]);
|
||||
|
||||
const constructSteps = () => {
|
||||
try {
|
||||
let optionsValue = isDynamicOptionsEnabled
|
||||
? component?.component?.definition?.properties?.schema?.value
|
||||
: component?.component?.definition?.properties?.steps?.value;
|
||||
let options = [];
|
||||
|
||||
if (isDynamicOptionsEnabled || typeof optionsValue === 'string') {
|
||||
options = getResolvedValue(optionsValue);
|
||||
} else {
|
||||
options = optionsValue?.map((option) => option);
|
||||
}
|
||||
return options.map((option) => {
|
||||
const newOption = { ...option };
|
||||
|
||||
Object.keys(option).forEach((key) => {
|
||||
if (typeof option[key]?.value === 'boolean') {
|
||||
newOption[key]['value'] = `{{${option[key]?.value}}}`;
|
||||
}
|
||||
});
|
||||
|
||||
if (!('visible' in newOption)) {
|
||||
newOption['visible'] = { value: '{{true}}' };
|
||||
}
|
||||
return newOption;
|
||||
});
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
let items = [];
|
||||
|
||||
items.push({
|
||||
title: 'Steps',
|
||||
isOpen: true,
|
||||
children: (
|
||||
<>
|
||||
{properties
|
||||
.filter((property) => !optionsProperties.includes(property))
|
||||
?.map((property) => {
|
||||
if (property === 'steps') {
|
||||
return (
|
||||
<>
|
||||
{renderElement(
|
||||
component,
|
||||
componentMeta,
|
||||
paramUpdated,
|
||||
dataQueries,
|
||||
'advanced',
|
||||
'properties',
|
||||
currentState,
|
||||
allComponents
|
||||
)}
|
||||
{isDynamicStepsEnabled
|
||||
? renderElement(
|
||||
component,
|
||||
componentMeta,
|
||||
paramUpdated,
|
||||
dataQueries,
|
||||
'schema',
|
||||
'properties',
|
||||
currentState,
|
||||
allComponents
|
||||
)
|
||||
: _renderOptions()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
// else if (property === 'variant') {
|
||||
// return renderTest(
|
||||
// component,
|
||||
// componentMeta,
|
||||
// paramUpdated,
|
||||
// dataQueries,
|
||||
// 'variant',
|
||||
// 'properties',
|
||||
// currentState,
|
||||
// allComponents,
|
||||
// handleLabelChange
|
||||
// );
|
||||
// }
|
||||
return renderElement(
|
||||
component,
|
||||
componentMeta,
|
||||
paramUpdated,
|
||||
dataQueries,
|
||||
property,
|
||||
'properties',
|
||||
currentState,
|
||||
allComponents,
|
||||
darkMode
|
||||
);
|
||||
})}
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
items.push({
|
||||
title: 'Events',
|
||||
isOpen: true,
|
||||
children: (
|
||||
<EventManager
|
||||
sourceId={component?.id}
|
||||
eventSourceType="component"
|
||||
eventMetaDefinition={componentMeta}
|
||||
dataQueries={dataQueries}
|
||||
components={allComponents}
|
||||
eventsChanged={eventsChanged}
|
||||
apps={apps}
|
||||
darkMode={darkMode}
|
||||
pages={pages}
|
||||
/>
|
||||
),
|
||||
});
|
||||
items.push({
|
||||
title: `Additional Actions`,
|
||||
isOpen: true,
|
||||
children: additionalActions.map((property) => {
|
||||
return renderElement(
|
||||
component,
|
||||
componentMeta,
|
||||
paramUpdated,
|
||||
dataQueries,
|
||||
property,
|
||||
'properties',
|
||||
currentState,
|
||||
allComponents,
|
||||
darkMode,
|
||||
componentMeta.properties?.[property]?.placeholder
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
items.push({
|
||||
title: 'Devices',
|
||||
isOpen: true,
|
||||
children: (
|
||||
<>
|
||||
{renderElement(
|
||||
component,
|
||||
componentMeta,
|
||||
layoutPropertyChanged,
|
||||
dataQueries,
|
||||
'showOnDesktop',
|
||||
'others',
|
||||
currentState,
|
||||
allComponents
|
||||
)}
|
||||
{renderElement(
|
||||
component,
|
||||
componentMeta,
|
||||
layoutPropertyChanged,
|
||||
dataQueries,
|
||||
'showOnMobile',
|
||||
'others',
|
||||
currentState,
|
||||
allComponents
|
||||
)}
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
return <Accordion items={items} />;
|
||||
}
|
||||
|
||||
function renderTest(...props) {
|
||||
const [
|
||||
component,
|
||||
componentMeta,
|
||||
paramUpdated,
|
||||
dataQueries,
|
||||
param,
|
||||
paramType,
|
||||
currentState,
|
||||
components = {},
|
||||
darkMode = false,
|
||||
placeholder = '',
|
||||
validationFn,
|
||||
] = props;
|
||||
const value = componentMeta?.definition?.properties?.variant?.value;
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Switch
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
paramUpdated({ name: 'variant' }, 'value', e, 'properties', false, props);
|
||||
}}
|
||||
meta={{
|
||||
...componentMeta.properties[param],
|
||||
fullWidth: true,
|
||||
}}
|
||||
paramName={param}
|
||||
isIcon={false}
|
||||
component={component.component.definition.name}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -255,7 +255,7 @@ export const PropertiesTabElements = ({
|
|||
paramType="properties"
|
||||
/>
|
||||
</div>
|
||||
{resolveReferences(column?.isEditable) && (
|
||||
{(column?.fxActiveFields?.includes('isEditable') || resolveReferences(column?.isEditable)) && (
|
||||
<ValidationProperties
|
||||
column={column}
|
||||
index={index}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
|
||||
import { ActionTypes } from '@/Editor/ActionTypes';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
|
|
@ -32,6 +32,9 @@ import useStore from '@/AppBuilder/_stores/store';
|
|||
import { useEventActions, useEvents } from '@/AppBuilder/_stores/slices/eventsSlice';
|
||||
import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup';
|
||||
import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
|
||||
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { components as selectComponents } from 'react-select';
|
||||
|
||||
export const EventManager = ({
|
||||
sourceId,
|
||||
|
|
@ -82,6 +85,8 @@ export const EventManager = ({
|
|||
|
||||
const [events, setEvents] = useState([]);
|
||||
const [focusedEventIndex, setFocusedEventIndex] = useState(null);
|
||||
const lastFocusedEventIndex = useRef(null);
|
||||
const shouldSkipOnToggle = useRef(null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -101,8 +106,23 @@ export const EventManager = ({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(currentEvents)]);
|
||||
|
||||
let actionOptions = ActionTypes.map((action) => {
|
||||
return { name: action.name, value: action.id };
|
||||
let groupedOptions = ActionTypes.reduce((acc, action) => {
|
||||
const groupName = action.group;
|
||||
|
||||
if (!acc[groupName]) {
|
||||
acc[groupName] = [];
|
||||
}
|
||||
|
||||
acc[groupName].push({
|
||||
label: action.name,
|
||||
value: action.id,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
let actionOptions = Object.keys(groupedOptions).map((groupName) => {
|
||||
return { label: groupName, options: groupedOptions[groupName] };
|
||||
});
|
||||
|
||||
let checkIfClicksAreInsideOf = document.querySelector('.cm-completionListIncompleteBottom');
|
||||
|
|
@ -124,6 +144,46 @@ export const EventManager = ({
|
|||
}),
|
||||
};
|
||||
|
||||
const actionStyles = {
|
||||
...styles,
|
||||
menuList: (base) => ({
|
||||
...base,
|
||||
padding: '8px 0 8px 8px',
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '10px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
background: 'transparent',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: '#E4E7EB',
|
||||
border: '1px solid transparent',
|
||||
backgroundClip: 'content-box',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
background: '#E4E7EB !important',
|
||||
border: '1px solid transparent !important',
|
||||
backgroundClip: 'content-box !important',
|
||||
},
|
||||
'&:hover': {
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: '#E4E7EB !important',
|
||||
border: '1px solid transparent !important',
|
||||
backgroundClip: 'content-box !important',
|
||||
},
|
||||
},
|
||||
}),
|
||||
group: (base) => ({
|
||||
...base,
|
||||
padding: 0,
|
||||
}),
|
||||
groupHeading: (base) => ({
|
||||
...base,
|
||||
margin: 0,
|
||||
padding: '0',
|
||||
}),
|
||||
};
|
||||
|
||||
const actionLookup = Object.fromEntries(ActionTypes.map((actionType) => [actionType.id, actionType]));
|
||||
|
||||
let alertTypes = [
|
||||
|
|
@ -394,6 +454,29 @@ export const EventManager = ({
|
|||
return defaultValue;
|
||||
};
|
||||
|
||||
const formatGroupLabel = (data) => {
|
||||
if (data.label === 'run-action') return;
|
||||
return (
|
||||
<div
|
||||
className="tw-border-x-0 tw-border-t-0 tw-border-b-[0.5px] tw-border-solid tw-my-[4px]"
|
||||
style={{ borderColor: 'var(--border-weak)' }}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomOption = (props) => {
|
||||
return (
|
||||
<selectComponents.Option {...props}>
|
||||
<div className="d-flex align-items-center">
|
||||
<div style={{ width: '16px', marginRight: '6px' }}>
|
||||
{props.isSelected && <SolidIcon name="tickv3" width="16px" height="16px" />}
|
||||
</div>
|
||||
<span>{props.label}</span>
|
||||
</div>
|
||||
</selectComponents.Option>
|
||||
);
|
||||
};
|
||||
|
||||
function eventPopover(event, index) {
|
||||
return (
|
||||
<Popover
|
||||
|
|
@ -433,13 +516,17 @@ export const EventManager = ({
|
|||
<Select
|
||||
className={`${darkMode ? 'select-search-dark' : 'select-search'} w-100`}
|
||||
options={actionOptions}
|
||||
value={event.actionId}
|
||||
value={actionOptions
|
||||
.flatMap((group) => group.options)
|
||||
.find((option) => option.value === event.actionId)}
|
||||
components={{ Option: CustomOption }}
|
||||
search={false}
|
||||
onChange={(value) => handlerChanged(index, 'actionId', value)}
|
||||
placeholder={t('globals.select', 'Select') + '...'}
|
||||
styles={styles}
|
||||
styles={actionStyles}
|
||||
useMenuPortal={false}
|
||||
useCustomStyles={true}
|
||||
formatGroupLabel={formatGroupLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1006,10 +1093,21 @@ export const EventManager = ({
|
|||
placement={popoverPlacement || 'left'}
|
||||
rootClose={true}
|
||||
overlay={eventPopover(event.event, index)}
|
||||
onHide={() => setFocusedEventIndex(null)}
|
||||
onToggle={(showing) => {
|
||||
// If the toggle action should be skipped (e.g., due to a previous state change), reset the flag and exit early.
|
||||
if (shouldSkipOnToggle.current) {
|
||||
shouldSkipOnToggle.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is already a focused event, set the skip flag to prevent unnecessary state updates.
|
||||
if (focusedEventIndex !== null && showing) {
|
||||
shouldSkipOnToggle.current = true;
|
||||
}
|
||||
|
||||
if (showing) {
|
||||
setFocusedEventIndex(index);
|
||||
lastFocusedEventIndex.current = index;
|
||||
} else {
|
||||
setFocusedEventIndex(null);
|
||||
}
|
||||
|
|
@ -1018,6 +1116,7 @@ export const EventManager = ({
|
|||
>
|
||||
<div
|
||||
key={index}
|
||||
id={`${sourceId}-${index}`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
|
|
@ -1061,6 +1160,17 @@ export const EventManager = ({
|
|||
);
|
||||
};
|
||||
|
||||
const shouldUsePopoverObserver = events.length !== 0 && eventSourceType === 'data_query';
|
||||
|
||||
usePopoverObserver(
|
||||
shouldUsePopoverObserver ? document.getElementsByClassName('query-details')[0] : null,
|
||||
document.getElementById(`${sourceId}-${lastFocusedEventIndex.current}`),
|
||||
document.getElementById('popover-basic'),
|
||||
focusedEventIndex !== null,
|
||||
() => (document.getElementById('popover-basic').style.display = 'block'),
|
||||
() => (document.getElementById('popover-basic').style.display = 'none')
|
||||
);
|
||||
|
||||
if (events.length === 0) {
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import Inspect from '@/_ui/Icon/solidIcons/Inspect';
|
|||
import classNames from 'classnames';
|
||||
import { EMPTY_ARRAY } from '@/_stores/editorStore';
|
||||
import { Select } from './Components/Select';
|
||||
import { Steps } from './Components/Steps.jsx';
|
||||
import { deepClone } from '@/_helpers/utilities/utils.helpers';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
// import { componentTypes } from '@/Editor/WidgetManager/components';
|
||||
|
|
@ -90,6 +91,7 @@ const NEW_REVAMPED_COMPONENTS = [
|
|||
'VerticalDivider',
|
||||
'ModalV2',
|
||||
'Link',
|
||||
'Steps',
|
||||
];
|
||||
|
||||
export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selectedComponentId }) => {
|
||||
|
|
@ -539,8 +541,8 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte
|
|||
componentMeta.displayName === 'Toggle Switch (Legacy)'
|
||||
? 'Toggle (Legacy)'
|
||||
: componentMeta.displayName === 'Toggle Switch'
|
||||
? 'Toggle Switch'
|
||||
: componentMeta.component,
|
||||
? 'Toggle Switch'
|
||||
: componentMeta.component,
|
||||
})}
|
||||
</small>
|
||||
</span>
|
||||
|
|
@ -740,6 +742,8 @@ const GetAccordion = React.memo(
|
|||
case 'DatePickerV2':
|
||||
case 'TimePicker':
|
||||
return <DatetimePickerV2 {...restProps} componentName={componentName} />;
|
||||
case 'Steps':
|
||||
return <Steps {...restProps} />;
|
||||
case 'PhoneInput':
|
||||
return <PhoneInput {...restProps} />;
|
||||
case 'CurrencyInput':
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ const NEW_WIDGETS = [
|
|||
'TimePicker',
|
||||
'ModalV2',
|
||||
'TextArea',
|
||||
'EmailInput',
|
||||
'PhoneInput',
|
||||
'CurrencyInput',
|
||||
];
|
||||
|
||||
export const WidgetBox = ({ component, darkMode }) => {
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
|
|||
showViewerNavigation={!isPagesSidebarHidden}
|
||||
handleAppEnvironmentChanged={handleAppEnvironmentChanged}
|
||||
changeToDarkMode={changeToDarkMode}
|
||||
switchPage={switchPage}
|
||||
/>
|
||||
)}
|
||||
<div className="sub-section">
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ export const containerConfig = {
|
|||
displayName: 'Container',
|
||||
description: 'Group components',
|
||||
defaultSize: {
|
||||
width: 10,
|
||||
height: 200,
|
||||
width: 13,
|
||||
height: 480,
|
||||
},
|
||||
component: 'Container',
|
||||
others: {
|
||||
|
|
|
|||
|
|
@ -4,25 +4,38 @@ export const stepsConfig = {
|
|||
description: 'Step-by-step navigation aid',
|
||||
component: 'Steps',
|
||||
properties: {
|
||||
variant: {
|
||||
type: 'switch',
|
||||
displayName: 'Variant',
|
||||
validation: { schema: { type: 'string' }, defaultValue: 'titles' },
|
||||
options: [
|
||||
{ displayName: 'Label', value: 'titles' },
|
||||
{ displayName: 'Number', value: 'numbers' },
|
||||
{ displayName: 'Plain', value: 'plain' },
|
||||
],
|
||||
accordian: 'label',
|
||||
},
|
||||
schema: {
|
||||
type: 'code',
|
||||
displayName: 'Schema',
|
||||
conditionallyRender: {
|
||||
key: 'advanced',
|
||||
value: true,
|
||||
},
|
||||
accordian: 'Options',
|
||||
},
|
||||
steps: {
|
||||
type: 'code',
|
||||
displayName: 'Steps',
|
||||
displayName: '',
|
||||
showLabel: false,
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'array',
|
||||
element: { type: 'object', object: { id: { type: 'number' } } },
|
||||
element: { type: 'object' },
|
||||
},
|
||||
defaultValue: `[{ name: 'step 1'}, {name: 'step 2'}]`,
|
||||
},
|
||||
},
|
||||
currentStep: {
|
||||
type: 'code',
|
||||
displayName: 'Current step',
|
||||
validation: {
|
||||
schema: { type: 'number' },
|
||||
defaultValue: 1,
|
||||
},
|
||||
},
|
||||
stepsSelectable: {
|
||||
type: 'toggle',
|
||||
displayName: 'Steps selectable',
|
||||
|
|
@ -30,7 +43,38 @@ export const stepsConfig = {
|
|||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
section: 'additionalActions',
|
||||
},
|
||||
disabledState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Disable',
|
||||
validation: { schema: { type: 'boolean' } },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
visibility: {
|
||||
type: 'toggle',
|
||||
displayName: 'Visibility',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
advanced: {
|
||||
type: 'toggle',
|
||||
displayName: 'Dynamic options',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: true,
|
||||
},
|
||||
accordian: 'Options',
|
||||
},
|
||||
currentStep: {
|
||||
type: 'code',
|
||||
displayName: 'Current step',
|
||||
validation: {
|
||||
schema: { type: 'number' },
|
||||
defaultValue: 1,
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
defaultSize: {
|
||||
width: 22,
|
||||
|
|
@ -40,46 +84,126 @@ export const stepsConfig = {
|
|||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
handle: 'setStep',
|
||||
displayName: 'Set step',
|
||||
params: [
|
||||
{
|
||||
handle: 'option',
|
||||
displayName: 'Option',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
handle: 'setVisibility',
|
||||
displayName: 'Set visibility',
|
||||
params: [{ handle: 'visible', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setDisabled',
|
||||
displayName: 'Set disabled',
|
||||
params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'resetSteps',
|
||||
displayName: 'Reset steps',
|
||||
params: [],
|
||||
},
|
||||
{
|
||||
handle: 'setStepVisible',
|
||||
displayName: 'Set step visible',
|
||||
params: [
|
||||
{
|
||||
handle: 'id',
|
||||
displayName: 'Step id',
|
||||
},
|
||||
{
|
||||
handle: 'visibility',
|
||||
displayName: 'visibility',
|
||||
defaultValue: '{{false}}',
|
||||
type: 'toggle',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
handle: 'setStepDisable',
|
||||
displayName: 'Set step disable',
|
||||
params: [
|
||||
{
|
||||
handle: 'id',
|
||||
displayName: 'Step id',
|
||||
},
|
||||
{
|
||||
handle: 'disabled',
|
||||
displayName: 'disabled',
|
||||
defaultValue: '{{true}}',
|
||||
type: 'toggle',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
events: {
|
||||
onSelect: { displayName: 'On select' },
|
||||
},
|
||||
styles: {
|
||||
color: {
|
||||
incompletedAccent: {
|
||||
type: 'colorSwatches',
|
||||
displayName: 'colorSwatches',
|
||||
displayName: 'Incompleted accent',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#CCD1D5',
|
||||
},
|
||||
accordian: 'steps',
|
||||
},
|
||||
incompletedLabel: {
|
||||
type: 'colorSwatches',
|
||||
displayName: 'Incompleted label',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#1B1F24',
|
||||
},
|
||||
accordian: 'steps',
|
||||
},
|
||||
completedAccent: {
|
||||
type: 'colorSwatches',
|
||||
displayName: 'Completed accent',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'var(--primary-brand)',
|
||||
},
|
||||
accordian: 'steps',
|
||||
},
|
||||
textColor: {
|
||||
completedLabel: {
|
||||
type: 'colorSwatches',
|
||||
displayName: 'Text color',
|
||||
displayName: 'Completed label',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#000000',
|
||||
defaultValue: '#1B1F24',
|
||||
},
|
||||
accordian: 'steps',
|
||||
},
|
||||
theme: {
|
||||
type: 'select',
|
||||
displayName: 'Theme',
|
||||
currentStepLabel: {
|
||||
type: 'colorSwatches',
|
||||
displayName: 'Current step label',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#1B1F24',
|
||||
},
|
||||
accordian: 'steps',
|
||||
},
|
||||
padding: {
|
||||
type: 'switch',
|
||||
displayName: 'Padding',
|
||||
validation: {
|
||||
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
|
||||
defaultValue: 'default',
|
||||
},
|
||||
options: [
|
||||
{ name: 'titles', value: 'titles' },
|
||||
{ name: 'numbers', value: 'numbers' },
|
||||
{ name: 'plain', value: 'plain' },
|
||||
{ displayName: 'Default', value: 'default' },
|
||||
{ displayName: 'None', value: 'none' },
|
||||
],
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'titles',
|
||||
},
|
||||
},
|
||||
visibility: {
|
||||
type: 'toggle',
|
||||
displayName: 'Visibility',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: true,
|
||||
},
|
||||
accordian: 'container',
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
|
|
@ -92,17 +216,35 @@ export const stepsConfig = {
|
|||
},
|
||||
properties: {
|
||||
steps: {
|
||||
value: `{{ [{ name: 'step 1', tooltip: 'some tooltip', id: 1},{ name: 'step 2', tooltip: 'some tooltip', id: 2},{ name: 'step 3', tooltip: 'some tooltip', id: 3},{ name: 'step 4', tooltip: 'some tooltip', id: 4},{ name: 'step 5', tooltip: 'some tooltip', id: 5}]}}`,
|
||||
value: [
|
||||
{ name: 'step 1', tooltip: '', id: 1, visible: { value: true }, disabled: { value: false } },
|
||||
{ name: 'step 2', tooltip: '', id: 2, visible: { value: true }, disabled: { value: false } },
|
||||
{ name: 'step 3', tooltip: '', id: 3, visible: { value: true }, disabled: { value: false } },
|
||||
{ name: 'step 4', tooltip: '', id: 4, visible: { value: true }, disabled: { value: false } },
|
||||
{ name: 'step 5', tooltip: '', id: 5, visible: { value: true }, disabled: { value: false } },
|
||||
],
|
||||
},
|
||||
schema: {
|
||||
value: `{{ [{ name: 'step 1', tooltip: '', id: 1,visible: true, disabled: false},{ name: 'step 2', tooltip: '', id: 2,visible: true, disabled: false},{ name: 'step 3', tooltip: '', id: 3,visible: true, disabled: false},{ name: 'step 4', tooltip: '', id: 4,visible: true, disabled: false},{ name: 'step 5', tooltip: '', id: 5,visible: true, disabled: false}]}}`,
|
||||
},
|
||||
disabledState: { value: '{{false}}' },
|
||||
variant: { value: 'titles' },
|
||||
currentStep: { value: '{{3}}' },
|
||||
stepsSelectable: { value: true },
|
||||
advanced: { value: `{{false}}` },
|
||||
visibility: { value: '{{true}}' },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
theme: { value: 'titles' },
|
||||
color: { value: 'var(--primary-brand)' },
|
||||
textColor: { value: '' },
|
||||
// color: { value: '' },
|
||||
// textColor: { value: '' },
|
||||
padding: { value: 'default' },
|
||||
incompletedAccent: { value: '#E4E7EB' },
|
||||
incompletedLabel: { value: '#1B1F24' },
|
||||
completedAccent: { value: 'var(--primary-brand)' },
|
||||
completedLabel: { value: '#1B1F24' },
|
||||
currentStepLabel: { value: '#1B1F24' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ function usePopoverObserver(containerRef, triggerRef, popoverRef, show, onShow,
|
|||
const prevShow = useRef(false);
|
||||
|
||||
// Check if it is a ref or a DOM element
|
||||
const container = containerRef?.current ? containerRef.current : containerRef;
|
||||
const trigger = triggerRef?.current ? triggerRef.current : triggerRef;
|
||||
const popover = popoverRef?.current ? popoverRef.current : popoverRef;
|
||||
const container = containerRef?.current !== undefined ? containerRef.current : containerRef;
|
||||
const trigger = triggerRef?.current !== undefined ? triggerRef.current : triggerRef;
|
||||
const popover = popoverRef?.current !== undefined ? popoverRef.current : popoverRef;
|
||||
|
||||
useEffect(() => {
|
||||
if (!container || !trigger) return;
|
||||
|
|
|
|||
|
|
@ -258,7 +258,11 @@ export const createDataQuerySlice = (set, get) => ({
|
|||
set((state) => {
|
||||
state.dataQuery.creatingQueryInProcessId = null;
|
||||
state.dataQuery.queries.modules[moduleId] = [
|
||||
{ ...data, data_source_id: queryToClone.data_source_id },
|
||||
{
|
||||
...data,
|
||||
data_source_id: queryToClone.data_source_id,
|
||||
plugin: { iconFile: queryToClone.plugin?.iconFile, icon_file: queryToClone.plugin?.icon_file },
|
||||
},
|
||||
...state.dataQuery.queries.modules[moduleId],
|
||||
];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ export const createResolvedSlice = (set, get) => ({
|
|||
'setVariables'
|
||||
);
|
||||
get().updateDependencyValues(`variables.${key}`);
|
||||
get().checkAndSetTrueBuildSuggestionsFlag();
|
||||
},
|
||||
|
||||
getVariable: (key, moduleId = 'canvas') => {
|
||||
|
|
@ -165,6 +166,7 @@ export const createResolvedSlice = (set, get) => ({
|
|||
'setPageVariable'
|
||||
);
|
||||
get().updateDependencyValues(`page.variables.${key}`);
|
||||
get().checkAndSetTrueBuildSuggestionsFlag();
|
||||
},
|
||||
|
||||
getPageVariable: (key, moduleId = 'canvas') => {
|
||||
|
|
|
|||
|
|
@ -1,62 +1,36 @@
|
|||
export const ActionTypes = [
|
||||
{
|
||||
name: 'Run query',
|
||||
id: 'run-query',
|
||||
options: [{ queryId: '' }],
|
||||
group: 'run-action',
|
||||
},
|
||||
{
|
||||
name: 'Show Alert',
|
||||
id: 'show-alert',
|
||||
options: [{ name: 'message', type: 'text', default: 'Message !' }],
|
||||
group: 'run-action',
|
||||
},
|
||||
{
|
||||
name: 'Logout',
|
||||
id: 'logout',
|
||||
},
|
||||
{
|
||||
name: 'Run Query',
|
||||
id: 'run-query',
|
||||
options: [{ queryId: '' }],
|
||||
},
|
||||
{
|
||||
name: 'Open Webpage',
|
||||
id: 'open-webpage',
|
||||
options: [{ name: 'url', type: 'text', default: 'https://example.com' }],
|
||||
},
|
||||
{
|
||||
name: 'Go to app',
|
||||
id: 'go-to-app',
|
||||
name: 'Control component',
|
||||
id: 'control-component',
|
||||
options: [
|
||||
{ name: 'app', type: 'text', default: '' },
|
||||
{ name: 'queryParams', type: 'code', default: '[]' },
|
||||
{ name: 'component', type: 'text', default: '' },
|
||||
{ name: 'action', type: 'text', default: '' },
|
||||
],
|
||||
group: 'control-component',
|
||||
},
|
||||
{
|
||||
name: 'Show Modal',
|
||||
name: 'Show modal',
|
||||
id: 'show-modal',
|
||||
options: [{ name: 'modal', type: 'text', default: '' }],
|
||||
group: 'control-component',
|
||||
},
|
||||
{
|
||||
name: 'Close Modal',
|
||||
name: 'Close modal',
|
||||
id: 'close-modal',
|
||||
options: [{ name: 'modal', type: 'text', default: '' }],
|
||||
},
|
||||
{
|
||||
name: 'Copy to clipboard',
|
||||
id: 'copy-to-clipboard',
|
||||
options: [{ name: 'copy-to-clipboard', type: 'text', default: '' }],
|
||||
},
|
||||
{
|
||||
name: 'Set local storage',
|
||||
id: 'set-localstorage-value',
|
||||
options: [
|
||||
{ name: 'key', type: 'code', default: '' },
|
||||
{ name: 'value', type: 'code', default: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Generate file',
|
||||
id: 'generate-file',
|
||||
options: [
|
||||
{ name: 'fileType', type: 'text', default: '' },
|
||||
{ name: 'fileName', type: 'text', default: '' },
|
||||
{ name: 'data', type: 'code', default: '{{[]}}' },
|
||||
],
|
||||
group: 'control-component',
|
||||
},
|
||||
{
|
||||
name: 'Set table page',
|
||||
|
|
@ -69,28 +43,28 @@ export const ActionTypes = [
|
|||
},
|
||||
{ name: 'pageIndex', type: 'text', default: '{{1}}' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Set variable',
|
||||
id: 'set-custom-variable',
|
||||
options: [
|
||||
{ name: 'key', type: 'code', default: '' },
|
||||
{ name: 'value', type: 'code', default: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Unset all variables',
|
||||
id: 'unset-all-custom-variables',
|
||||
},
|
||||
{
|
||||
name: 'Unset variable',
|
||||
id: 'unset-custom-variable',
|
||||
options: [{ name: 'key', type: 'code', default: '' }],
|
||||
group: 'control-component',
|
||||
},
|
||||
{
|
||||
name: 'Switch page',
|
||||
id: 'switch-page',
|
||||
options: [{ name: 'page', type: 'text', default: '' }],
|
||||
group: 'navigation',
|
||||
},
|
||||
{
|
||||
name: 'Go to app',
|
||||
id: 'go-to-app',
|
||||
options: [
|
||||
{ name: 'app', type: 'text', default: '' },
|
||||
{ name: 'queryParams', type: 'code', default: '[]' },
|
||||
],
|
||||
group: 'navigation',
|
||||
},
|
||||
{
|
||||
name: 'Open webpage',
|
||||
id: 'open-webpage',
|
||||
options: [{ name: 'url', type: 'text', default: 'https://example.com' }],
|
||||
group: 'navigation',
|
||||
},
|
||||
{
|
||||
name: 'Set page variable',
|
||||
|
|
@ -99,10 +73,7 @@ export const ActionTypes = [
|
|||
{ name: 'key', type: 'code', default: '' },
|
||||
{ name: 'value', type: 'code', default: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Unset all page variables',
|
||||
id: 'unset-all-page-variables',
|
||||
group: 'variable',
|
||||
},
|
||||
{
|
||||
name: 'Unset page variable',
|
||||
|
|
@ -111,14 +82,61 @@ export const ActionTypes = [
|
|||
{ name: 'key', type: 'code', default: '' },
|
||||
{ name: 'value', type: 'code', default: '' },
|
||||
],
|
||||
group: 'variable',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Control component',
|
||||
id: 'control-component',
|
||||
name: 'Unset all page variables',
|
||||
id: 'unset-all-page-variables',
|
||||
group: 'variable',
|
||||
},
|
||||
{
|
||||
name: 'Set variable',
|
||||
id: 'set-custom-variable',
|
||||
options: [
|
||||
{ name: 'component', type: 'text', default: '' },
|
||||
{ name: 'action', type: 'text', default: '' },
|
||||
{ name: 'key', type: 'code', default: '' },
|
||||
{ name: 'value', type: 'code', default: '' },
|
||||
],
|
||||
group: 'variable',
|
||||
},
|
||||
{
|
||||
name: 'Unset variable',
|
||||
id: 'unset-custom-variable',
|
||||
options: [{ name: 'key', type: 'code', default: '' }],
|
||||
group: 'variable',
|
||||
},
|
||||
{
|
||||
name: 'Unset all variables',
|
||||
id: 'unset-all-custom-variables',
|
||||
group: 'variable',
|
||||
},
|
||||
{
|
||||
name: 'Logout',
|
||||
id: 'logout',
|
||||
group: 'other',
|
||||
},
|
||||
{
|
||||
name: 'Generate file',
|
||||
id: 'generate-file',
|
||||
options: [
|
||||
{ name: 'fileType', type: 'text', default: '' },
|
||||
{ name: 'fileName', type: 'text', default: '' },
|
||||
{ name: 'data', type: 'code', default: '{{[]}}' },
|
||||
],
|
||||
group: 'other',
|
||||
},
|
||||
{
|
||||
name: 'Set local storage',
|
||||
id: 'set-localstorage-value',
|
||||
options: [
|
||||
{ name: 'key', type: 'code', default: '' },
|
||||
{ name: 'value', type: 'code', default: '' },
|
||||
],
|
||||
group: 'other',
|
||||
},
|
||||
{
|
||||
name: 'Copy to clipboard',
|
||||
id: 'copy-to-clipboard',
|
||||
options: [{ name: 'copy-to-clipboard', type: 'text', default: '' }],
|
||||
group: 'other',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
/* eslint-disable react/no-string-refs */
|
||||
import React from 'react';
|
||||
import { Editor, EditorState, RichUtils, getDefaultKeyBinding, ContentState, convertFromHTML } from 'draft-js';
|
||||
import { Editor, EditorState, RichUtils, getDefaultKeyBinding } from 'draft-js';
|
||||
import 'draft-js/dist/Draft.css';
|
||||
import { stateFromHTML } from 'draft-js-import-html';
|
||||
import { stateToHTML } from 'draft-js-export-html';
|
||||
import Loader from '@/ToolJetUI/Loader/Loader';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
|
@ -150,11 +151,8 @@ const InlineStyleControls = (props) => {
|
|||
class DraftEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(this.props.defaultValue));
|
||||
this.state = {
|
||||
editorState: EditorState.createWithContent(
|
||||
ContentState.createFromBlockArray(blocksFromHTML.contentBlocks, blocksFromHTML.entityMap)
|
||||
),
|
||||
editorState: EditorState.createWithContent(stateFromHTML(DOMPurify.sanitize(this.props.defaultValue))),
|
||||
};
|
||||
|
||||
this.editorContainerRef = React.createRef();
|
||||
|
|
@ -173,6 +171,18 @@ class DraftEditor extends React.Component {
|
|||
this.toggleInlineStyle = this._toggleInlineStyle.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.defaultValue !== this.props.defaultValue) {
|
||||
const newContentState = stateFromHTML(DOMPurify.sanitize(this.props.defaultValue));
|
||||
const newEditorState = EditorState.createWithContent(newContentState);
|
||||
const html = stateToHTML(newContentState);
|
||||
|
||||
this.props.handleChange(html);
|
||||
|
||||
this.setState({ editorState: newEditorState });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
//For resizing the editor container based on the height of rich text editor controls
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
|
|
@ -193,11 +203,7 @@ class DraftEditor extends React.Component {
|
|||
isVisible: this.props.isVisible,
|
||||
isLoading: this.props.isLoading,
|
||||
setValue: async (text) => {
|
||||
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(text));
|
||||
const newContentState = ContentState.createFromBlockArray(
|
||||
blocksFromHTML.contentBlocks,
|
||||
blocksFromHTML.entityMap
|
||||
);
|
||||
const newContentState = stateFromHTML(DOMPurify.sanitize(text));
|
||||
const newEditorState = EditorState.createWithContent(newContentState);
|
||||
const html = stateToHTML(newContentState);
|
||||
this.props.handleChange(html);
|
||||
|
|
@ -226,19 +232,6 @@ class DraftEditor extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.defaultValue !== this.props.defaultValue) {
|
||||
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(this.props.defaultValue));
|
||||
const newContentState = ContentState.createFromBlockArray(blocksFromHTML.contentBlocks, blocksFromHTML.entityMap);
|
||||
const newEditorState = EditorState.createWithContent(newContentState);
|
||||
const html = stateToHTML(newContentState);
|
||||
|
||||
this.props.handleChange(html);
|
||||
|
||||
this.setState({ editorState: newEditorState });
|
||||
}
|
||||
}
|
||||
|
||||
_handleKeyCommand(command, editorState) {
|
||||
const newState = RichUtils.handleKeyCommand(editorState, command);
|
||||
if (newState) {
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ export const RadioButtonV2 = ({
|
|||
data-cy={`label-${String(componentName).toLowerCase()} `}
|
||||
data-disabled={isDisabled}
|
||||
id={String(componentName)}
|
||||
className={cx('radio-button,', 'd-flex', {
|
||||
className={cx('radio-button', 'd-flex', {
|
||||
[alignment === 'top' &&
|
||||
((labelWidth != 0 && label?.length != 0) ||
|
||||
(labelAutoWidth && labelWidth == 0 && label && label?.length != 0))
|
||||
|
|
@ -279,7 +279,7 @@ export const RadioButtonV2 = ({
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${isValid ? '' : visibility ? 'd-flex' : 'none'}`}
|
||||
className={`${isValid ? 'd-none' : visibility ? 'd-flex' : 'd-none'}`}
|
||||
style={{
|
||||
color: 'var(--status-error-strong)',
|
||||
justifyContent: direction === 'right' ? 'flex-start' : 'flex-end',
|
||||
|
|
|
|||
|
|
@ -1,53 +1,226 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { isExpectedDataType } from '@/_helpers/utils';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import './Steps.scss';
|
||||
|
||||
export const Steps = function Button({ properties, styles, fireEvent, setExposedVariable, height, darkMode, dataCy }) {
|
||||
const { stepsSelectable } = properties;
|
||||
const currentStep = isExpectedDataType(properties.currentStep, 'number');
|
||||
const steps = isExpectedDataType(properties.steps, 'array');
|
||||
const { color, theme, visibility, boxShadow } = styles;
|
||||
export const Steps = function Steps({ properties, styles, fireEvent, setExposedVariable, height, darkMode, dataCy }) {
|
||||
const { stepsSelectable, disabledState } = properties;
|
||||
const visibility = isExpectedDataType(properties.visibility, 'boolean');
|
||||
const currentStepId = isExpectedDataType(properties.currentStep, 'number');
|
||||
const isDynamicStepsEnabled = isExpectedDataType(properties.advanced, 'boolean');
|
||||
const steps = isDynamicStepsEnabled ? properties.schema : properties.steps;
|
||||
const { color, boxShadow } = styles;
|
||||
const textColor = darkMode && styles.textColor === '#000' ? '#fff' : styles.textColor;
|
||||
const [activeStep, setActiveStep] = useState(null);
|
||||
const { completedAccent, incompletedAccent, incompletedLabel, completedLabel, currentStepLabel } = styles;
|
||||
const [stepsArr, setStepsArr] = useState(steps);
|
||||
const [isVisible, setIsVisible] = useState(visibility);
|
||||
const [isDisabled, setIsDisabled] = useState(disabledState);
|
||||
const [activeStepId, setActiveStepId] = useState(currentStepId);
|
||||
const theme = properties.variant;
|
||||
const [progressBarWidth, setProgressBarWidth] = useState(0);
|
||||
const [containerPadding, setContainerPadding] = useState(0);
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
const [filteredSteps, setFilteredSteps] = useState([]);
|
||||
const firstLabelRef = useRef(null);
|
||||
const lastLabelRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const currentStepIndex = filteredSteps.findIndex((step) => step.id == activeStepId);
|
||||
|
||||
useEffect(() => {
|
||||
const sanitizedSteps = JSON.parse(JSON.stringify(steps || [])).map((step) => ({
|
||||
...step,
|
||||
visible: 'visible' in step ? step.visible : true,
|
||||
disabled: 'disabled' in step ? step.disabled : false,
|
||||
}));
|
||||
const newFilteredSteps = (sanitizedSteps || []).filter((step) => step.visible);
|
||||
setFilteredSteps(newFilteredSteps);
|
||||
setStepsArr(sanitizedSteps);
|
||||
}, [JSON.stringify(steps)]);
|
||||
|
||||
// Common function to calculate progress bar width and label padding
|
||||
const calculateProgressBarWidth = () => {
|
||||
if (!containerRef.current || theme !== 'titles') return;
|
||||
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
setContainerWidth(containerWidth);
|
||||
|
||||
const stepWidth = 20; // width of dot + padding
|
||||
const totalStepsWidth = filteredSteps.length * stepWidth;
|
||||
const totalProgressBars = filteredSteps.length - 1;
|
||||
|
||||
if (filteredSteps.length === 1) {
|
||||
setProgressBarWidth(containerWidth);
|
||||
setContainerPadding(0); // No padding needed for single step
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate progress bar width
|
||||
const progressBarWidth = (containerWidth - totalStepsWidth) / totalProgressBars;
|
||||
setProgressBarWidth(Math.min(progressBarWidth, (containerWidth - totalStepsWidth) / filteredSteps.length));
|
||||
|
||||
// Calculate container padding
|
||||
if (firstLabelRef.current && lastLabelRef.current) {
|
||||
const labelWidth = (containerWidth - (filteredSteps.length - 1) - 4) / filteredSteps.length;
|
||||
|
||||
const firstLabelWidth = firstLabelRef.current.offsetWidth;
|
||||
const lastLabelWidth = lastLabelRef.current.offsetWidth;
|
||||
const maxLabelWidth = Math.max(firstLabelWidth, lastLabelWidth);
|
||||
|
||||
const calculatedPadding = (maxLabelWidth / 2) - 1;
|
||||
setContainerPadding(Math.max(2, calculatedPadding)); // Ensure minimum padding of 2px
|
||||
}
|
||||
};
|
||||
|
||||
// Add resize observer to track container width and calculate progress bar width
|
||||
useEffect(() => {
|
||||
calculateProgressBarWidth();
|
||||
if (theme !== 'titles') return;
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
calculateProgressBarWidth();
|
||||
}
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [theme, JSON.stringify(steps), filteredSteps]);
|
||||
// Dynamic styles for theming
|
||||
const dynamicStyle = {
|
||||
'--bgColor': styles.color,
|
||||
'--textColor': textColor,
|
||||
};
|
||||
const activeStepHandler = (id) => {
|
||||
const active = steps.filter((item) => item.id == id);
|
||||
setExposedVariable('currentStepId', active[0].id);
|
||||
fireEvent('onSelect');
|
||||
setActiveStep(active[0].id);
|
||||
'--completedAccent': completedAccent === '#4368E3' ? 'var(--primary-brand)' : completedAccent,
|
||||
'--incompletedAccent': incompletedAccent === '#E4E7EB' ? 'var(--surfaces-surface-03)' : incompletedAccent,
|
||||
'--incompletedLabel': incompletedLabel === '#1B1F24' ? 'var(--text-primary)' : incompletedLabel,
|
||||
'--completedLabel': completedLabel === '#1B1F24' ? 'var(--text-primary)' : completedLabel,
|
||||
'--currentStepLabel': currentStepLabel === '#1B1F24' ? 'var(--text-primary)' : currentStepLabel,
|
||||
};
|
||||
|
||||
// Step click handler
|
||||
const handleStepClick = (id) => {
|
||||
const step = filteredSteps.find((item) => item.id == id);
|
||||
if (step && !step.disabled && !isDisabled) {
|
||||
setActiveStepId(step.id);
|
||||
fireEvent('onSelect');
|
||||
}
|
||||
};
|
||||
|
||||
// Expose variables and methods
|
||||
useEffect(() => {
|
||||
setActiveStep(currentStep);
|
||||
setExposedVariable('currentStepId', currentStep);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentStep]);
|
||||
setExposedVariable('isVisible', isVisible);
|
||||
setExposedVariable('isDisabled', isDisabled);
|
||||
setExposedVariable('currentStepId', activeStepId);
|
||||
setExposedVariable('steps', stepsArr);
|
||||
|
||||
setExposedVariable('setStepVisible', (stepId, visibility) => {
|
||||
setStepsArr((prev) => {
|
||||
const updatedSteps = prev.map((item) =>
|
||||
item.id == stepId ? { ...item, visible: visibility } : item
|
||||
);
|
||||
setExposedVariable('steps', updatedSteps);
|
||||
return updatedSteps;
|
||||
});
|
||||
});
|
||||
|
||||
setExposedVariable('setStepDisable', (stepId, disabled) => {
|
||||
setStepsArr((prev) => {
|
||||
const updatedSteps = prev.map((item) =>
|
||||
item.id == stepId ? { ...item, disabled: disabled } : item
|
||||
);
|
||||
setExposedVariable('steps', updatedSteps);
|
||||
return updatedSteps;
|
||||
});
|
||||
});
|
||||
|
||||
setExposedVariable('resetSteps', () => {
|
||||
setActiveStepId(stepsArr.filter((step) => step.visible)?.[0]?.id);
|
||||
});
|
||||
|
||||
setExposedVariable('setStep', (stepId) => {
|
||||
if (!disabledState) setActiveStepId(stepId);
|
||||
});
|
||||
setExposedVariable('setVisibility', (visibility) => setIsVisible(visibility));
|
||||
setExposedVariable('setDisable', (disabled) => setIsDisabled(disabled));
|
||||
}, [isVisible, isDisabled, activeStepId, stepsArr, disabledState]);
|
||||
|
||||
// Update state from props
|
||||
useEffect(() => setIsVisible(visibility), [visibility]);
|
||||
useEffect(() => setIsDisabled(disabledState), [disabledState]);
|
||||
useEffect(() => setActiveStepId(currentStepId), [currentStepId]);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
visibility && (
|
||||
<div
|
||||
className={`steps ${theme == 'numbers' && 'steps-counter '}`}
|
||||
style={{ color: textColor, height, boxShadow }}
|
||||
data-cy={dataCy}
|
||||
>
|
||||
{steps?.map((item) => (
|
||||
<a
|
||||
key={item.id}
|
||||
className={`step-item ${item.id == activeStep && 'active'} ${!stepsSelectable && 'step-item-disabled'} ${
|
||||
color && `step-${color}`
|
||||
}`}
|
||||
data-bs-toggle="tooltip"
|
||||
title={item?.tooltip}
|
||||
onClick={() => stepsSelectable && activeStepHandler(item.id)}
|
||||
style={dynamicStyle}
|
||||
>
|
||||
{theme == 'titles' && item.name}
|
||||
</a>
|
||||
))}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`steps-container ${isDisabled ? 'disabled' : ''} ${filteredSteps.length === 1 ? 'single-step' : ''}`}
|
||||
style={{
|
||||
height,
|
||||
boxShadow,
|
||||
padding: theme === 'titles' ? `0 ${containerPadding}px` : 2,
|
||||
paddingTop: theme === 'plain' ? `3px` : theme === 'numbers' ? `2px` : 0,
|
||||
...dynamicStyle
|
||||
}}
|
||||
data-cy={dataCy}
|
||||
>
|
||||
<div className={`progress-line-container ${filteredSteps.length === 1 ? 'single-step' : ''}`}>
|
||||
{filteredSteps.map((step, index) => {
|
||||
const isStepDisabled = step.disabled;
|
||||
const isCompleted = index < currentStepIndex;
|
||||
const isActive = index === currentStepIndex;
|
||||
const isUpcoming = index > currentStepIndex;
|
||||
const isFirstStep = index === 0;
|
||||
const isLastStep = index === filteredSteps.length - 1;
|
||||
|
||||
return (
|
||||
<React.Fragment key={index}> {/* using index as key to avoid issues due to duplicate step ids */}
|
||||
<ToolTip
|
||||
show={!step.disabled && !isDisabled && step.tooltip}
|
||||
message={step.tooltip || ''}
|
||||
>
|
||||
<div
|
||||
onClick={() => stepsSelectable && handleStepClick(step.id)}
|
||||
className={`milestone ${theme === 'numbers' ? 'numbers' : ''} ${isDisabled || isStepDisabled ? 'disabled' : ''
|
||||
} ${isCompleted ? 'completed' : isActive ? 'active' : 'incomplete'}`}
|
||||
>
|
||||
{theme === 'numbers' ? (
|
||||
index + 1
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`dot ${isCompleted ? 'completed' : isActive ? 'active' : 'incomplete'}`}
|
||||
style={{
|
||||
border: `2px solid ${isCompleted ? completedAccent : isActive ? completedAccent : incompletedAccent}`,
|
||||
backgroundColor: isActive ? 'transparent' : (isCompleted ? completedAccent : incompletedAccent)
|
||||
}}
|
||||
/>
|
||||
{theme === 'titles' && (
|
||||
<div
|
||||
ref={isFirstStep ? firstLabelRef : isLastStep ? lastLabelRef : null}
|
||||
className={`label ${isCompleted ? 'completed' : isActive ? 'active' : 'incomplete'}`}
|
||||
style={{ maxWidth: `${progressBarWidth}px` }}
|
||||
>
|
||||
{step.name}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ToolTip>
|
||||
|
||||
{index < filteredSteps.length - 1 && (
|
||||
<div
|
||||
className={`step-connector ${isCompleted ? 'completed' : 'incomplete'}`}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
132
frontend/src/Editor/Components/Steps.scss
Normal file
132
frontend/src/Editor/Components/Steps.scss
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
.steps-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.single-step {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-line-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
&.single-step {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.milestone {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&.numbers {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
box-sizing: content-box;
|
||||
|
||||
&.completed {
|
||||
background-color: var(--completedAccent);
|
||||
color: var(--completedLabel);
|
||||
border: 2px solid var(--completedAccent);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--currentStepLabel);
|
||||
border: 2px solid var(--completedAccent);
|
||||
}
|
||||
|
||||
&.incomplete {
|
||||
background-color: var(--incompletedAccent);
|
||||
color: var(--incompletedLabel);
|
||||
border: 2px solid var(--incompletedAccent);
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: content-box;
|
||||
|
||||
&.completed {
|
||||
background-color: var(--completedAccent);
|
||||
border: 2px solid var(--completedAccent);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: white;
|
||||
border: 2px solid var(--primary-brand);
|
||||
}
|
||||
|
||||
&.incomplete {
|
||||
background-color: var(--incompletedAccent);
|
||||
border: 2px solid var(--incompletedAccent);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
margin-top: 2px;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: max-content;
|
||||
|
||||
&.completed {
|
||||
color: var(--completedLabel);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--completedLabel);
|
||||
}
|
||||
|
||||
&.incomplete {
|
||||
color: var(--incompletedLabel);
|
||||
}
|
||||
}
|
||||
|
||||
.step-connector {
|
||||
flex-grow: 1;
|
||||
height: 2px;
|
||||
align-self: center;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.completed {
|
||||
background-color: var(--completedAccent);
|
||||
}
|
||||
|
||||
&.incomplete {
|
||||
background-color: var(--incompletedAccent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,8 @@ export const containerConfig = {
|
|||
displayName: 'Container',
|
||||
description: 'Group components',
|
||||
defaultSize: {
|
||||
width: 10,
|
||||
height: 200,
|
||||
width: 13,
|
||||
height: 480,
|
||||
},
|
||||
component: 'Container',
|
||||
others: {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ import { verticalDividerConfig } from './verticalDivider';
|
|||
import { customComponentConfig } from './customComponent';
|
||||
import { buttonGroupConfig } from './buttonGroup';
|
||||
import { pdfConfig } from './pdf';
|
||||
import { stepsConfig } from './steps';
|
||||
// import { stepsConfig } from './steps';
|
||||
import { kanbanConfig } from './kanban';
|
||||
import { colorPickerConfig } from './colorPicker';
|
||||
import { treeSelectConfig } from './treeSelect';
|
||||
|
|
@ -106,7 +106,7 @@ export {
|
|||
customComponentConfig,
|
||||
buttonGroupConfig,
|
||||
pdfConfig,
|
||||
stepsConfig,
|
||||
// stepsConfig,
|
||||
kanbanConfig,
|
||||
kanbanBoardConfig, //!Depreciated
|
||||
colorPickerConfig,
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
export const stepsConfig = {
|
||||
name: 'Steps',
|
||||
displayName: 'Steps',
|
||||
description: 'Step-by-step navigation aid',
|
||||
component: 'Steps',
|
||||
properties: {
|
||||
steps: {
|
||||
type: 'code',
|
||||
displayName: 'Steps',
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'array',
|
||||
element: { type: 'object', object: { id: { type: 'number' } } },
|
||||
},
|
||||
defaultValue: `[{ name: 'step 1'}, {name: 'step 2'}]`,
|
||||
},
|
||||
},
|
||||
currentStep: {
|
||||
type: 'code',
|
||||
displayName: 'Current step',
|
||||
validation: {
|
||||
schema: { type: 'number' },
|
||||
defaultValue: 1,
|
||||
},
|
||||
},
|
||||
stepsSelectable: {
|
||||
type: 'toggle',
|
||||
displayName: 'Steps selectable',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultSize: {
|
||||
width: 22,
|
||||
height: 38,
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
events: {
|
||||
onSelect: { displayName: 'On select' },
|
||||
},
|
||||
styles: {
|
||||
color: {
|
||||
type: 'color',
|
||||
displayName: 'Color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#000000',
|
||||
},
|
||||
},
|
||||
textColor: {
|
||||
type: 'color',
|
||||
displayName: 'Text color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#000000',
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
type: 'select',
|
||||
displayName: 'Theme',
|
||||
options: [
|
||||
{ name: 'titles', value: 'titles' },
|
||||
{ name: 'numbers', value: 'numbers' },
|
||||
{ name: 'plain', value: 'plain' },
|
||||
],
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'titles',
|
||||
},
|
||||
},
|
||||
visibility: {
|
||||
type: 'toggle',
|
||||
displayName: 'Visibility',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
currentStepId: '3',
|
||||
},
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: '{{true}}' },
|
||||
showOnMobile: { value: '{{false}}' },
|
||||
},
|
||||
properties: {
|
||||
steps: {
|
||||
value: `{{ [{ name: 'step 1', tooltip: 'some tooltip', id: 1},{ name: 'step 2', tooltip: 'some tooltip', id: 2},{ name: 'step 3', tooltip: 'some tooltip', id: 3},{ name: 'step 4', tooltip: 'some tooltip', id: 4},{ name: 'step 5', tooltip: 'some tooltip', id: 5}]}}`,
|
||||
},
|
||||
currentStep: { value: '{{3}}' },
|
||||
stepsSelectable: { value: true },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
theme: { value: 'titles' },
|
||||
color: { value: '' },
|
||||
textColor: { value: '' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -13,6 +13,7 @@ export const organizationService = {
|
|||
getWorkspacesLimit,
|
||||
checkWorkspaceUniqueness,
|
||||
updateOrganization,
|
||||
setDefaultWorkspace,
|
||||
};
|
||||
|
||||
function getUsersByValue(searchInput) {
|
||||
|
|
@ -100,3 +101,8 @@ function checkWorkspaceUniqueness(name, slug) {
|
|||
const query = queryString.stringify({ name, slug });
|
||||
return fetch(`${config.apiUrl}/organizations/is-unique?${query}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function setDefaultWorkspace(workspaceId) {
|
||||
const requestOptions = { method: 'PATCH', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/organizations/${workspaceId}/default`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7675,29 +7675,33 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 4px ;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.rounded-0 {
|
||||
border-radius: 0 !important
|
||||
}
|
||||
|
||||
.rounded-top-left{
|
||||
.rounded-top-left {
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
|
||||
.rounded-top-left-0{
|
||||
.rounded-top-left-0 {
|
||||
border-top-left-radius: 0 !important;
|
||||
}
|
||||
.rounded-top-right-0{
|
||||
|
||||
.rounded-top-right-0 {
|
||||
border-top-right-radius: 0 !important;
|
||||
}
|
||||
.rounded-bottom-left-0{
|
||||
|
||||
.rounded-bottom-left-0 {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.rounded-bottom-right-0{
|
||||
|
||||
.rounded-bottom-right-0 {
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.rounded-1 {
|
||||
border-radius: 2px !important
|
||||
}
|
||||
|
|
@ -17484,8 +17488,8 @@ a.step-item:hover {
|
|||
|
||||
.step-item:not(:first-child):after {
|
||||
position: absolute;
|
||||
left: -50%;
|
||||
width: 100%;
|
||||
left: calc(-50% + 8px);
|
||||
width: calc(100% - 16px);
|
||||
content: "";
|
||||
transform: translateY(-50%)
|
||||
}
|
||||
|
|
@ -17498,13 +17502,25 @@ a.step-item:hover {
|
|||
box-sizing: content-box;
|
||||
display: block;
|
||||
content: "";
|
||||
border: 2px solid #fff;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%)
|
||||
}
|
||||
|
||||
.step-item.active {
|
||||
font-weight: 600
|
||||
.steps.steps-counter {
|
||||
.step-item:not(:first-child):after {
|
||||
left: calc(-50% + 16px) !important;
|
||||
width: calc(100% - 32px) !important;
|
||||
}
|
||||
}
|
||||
.steps-counter .step-item:before {
|
||||
color:var(--completedLabel) !important;
|
||||
}
|
||||
.steps .step-item.active:before{
|
||||
color : var(--currentStepLabel) !important;
|
||||
}
|
||||
.step-item {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.step-item.active:before {
|
||||
|
|
@ -17521,7 +17537,7 @@ a.step-item:hover {
|
|||
}
|
||||
|
||||
.step-item.active~.step-item:before {
|
||||
color: #656d77 !important
|
||||
color: var(--incompletedLabel) !important
|
||||
}
|
||||
|
||||
.steps-counter {
|
||||
|
|
@ -17549,7 +17565,8 @@ a.step-item:hover {
|
|||
.steps-counter .step-item:before {
|
||||
font-size: .75rem;
|
||||
line-height: 1.5rem;
|
||||
content: counter(steps)
|
||||
content: counter(steps);
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.steps-counter .step-item.active~.step-item:before {
|
||||
|
|
@ -19156,4 +19173,4 @@ img {
|
|||
background: #1f2936;
|
||||
border-color: #dadcde
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
23
frontend/src/_ui/Icon/solidIcons/Moon.jsx
Normal file
23
frontend/src/_ui/Icon/solidIcons/Moon.jsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
|
||||
const Moon = ({ fill = '#6A727C', width = '24', className = '', viewBox = '0 0 24 24' }) => {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
className={className}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16.107 6.64283C15.6139 6.64283 15.2142 6.24309 15.2142 5.74998V5.21426H14.6785C14.1854 5.21426 13.7856 4.81452 13.7856 4.32141C13.7856 3.8283 14.1854 3.42856 14.6785 3.42856H15.2142V2.89285C15.2142 2.39974 15.6139 2 16.107 2C16.6002 2 16.9999 2.39974 16.9999 2.89285V3.42856H17.5356C18.0288 3.42856 18.4285 3.8283 18.4285 4.32141C18.4285 4.81452 18.0288 5.21426 17.5356 5.21426H16.9999V5.74998C16.9999 6.24309 16.6002 6.64283 16.107 6.64283ZM18.7856 11.4642C18.7856 11.9573 19.1853 12.3571 19.6785 12.3571C20.1716 12.3571 20.5713 11.9573 20.5713 11.4642V10.9285H21.107C21.6002 10.9285 21.9999 10.5288 21.9999 10.0357C21.9999 9.54255 21.6002 9.14281 21.107 9.14281H20.5713V8.6071C20.5713 8.11399 20.1716 7.71425 19.6785 7.71425C19.1853 7.71425 18.7856 8.11399 18.7856 8.6071V9.14281H18.2499C17.7568 9.14281 17.357 9.54255 17.357 10.0357C17.357 10.5288 17.7568 10.9285 18.2499 10.9285H18.7856V11.4642ZM6.07263 6.07373C7.96356 4.98201 10.1187 4.74735 12.1075 5.23996C12.7387 5.39633 12.9939 5.95217 12.9996 6.40209C13.0051 6.8352 12.7967 7.33059 12.3328 7.59845C10.4674 8.67541 9.82636 11.1468 10.9769 13.1396C12.1274 15.1324 14.5883 15.8129 16.4536 14.736C16.9176 14.4681 17.4508 14.5354 17.8232 14.7567C18.2099 14.9866 18.5637 15.4855 18.3835 16.1103C17.8158 18.0789 16.5349 19.828 14.644 20.9197C10.6598 23.2201 5.53735 21.7172 3.18302 17.6395C0.828705 13.5616 2.08841 8.37403 6.07263 6.07373Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Moon;
|
||||
|
|
@ -234,6 +234,7 @@ import NewTabSmall from './NewTabSmall.jsx';
|
|||
import Code from './Code.jsx';
|
||||
import WorkflowV3 from './WorkflowV3.jsx';
|
||||
import WorkspaceV3 from './WorkspaceV3.jsx';
|
||||
import Moon from './Moon.jsx';
|
||||
|
||||
const Icon = (props) => {
|
||||
switch (props.name) {
|
||||
|
|
@ -707,6 +708,8 @@ const Icon = (props) => {
|
|||
return <AICrown {...props} />;
|
||||
case 'play01':
|
||||
return <Play01 {...props} />;
|
||||
case 'moon':
|
||||
return <Moon {...props} />;
|
||||
default:
|
||||
return <Apps {...props} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export const SelectComponent = ({ options = [], value, onChange, closeMenuOnSele
|
|||
isDisabled = false,
|
||||
borderRadius,
|
||||
openMenuOnFocus = false,
|
||||
customClassPrefix = '',
|
||||
} = restProps;
|
||||
|
||||
const customStyles = useCustomStyles ? styles : defaultStyles(isDarkMode, width, height, styles, borderRadius);
|
||||
|
|
@ -74,7 +75,7 @@ export const SelectComponent = ({ options = [], value, onChange, closeMenuOnSele
|
|||
maxMenuHeight={maxMenuHeight}
|
||||
menuPortalTarget={useMenuPortal ? document.body : menuPortalTarget}
|
||||
closeMenuOnSelect={closeMenuOnSelect ?? true}
|
||||
classNamePrefix={`${isDarkMode && 'dark-theme'} ${'react-select'}`}
|
||||
classNamePrefix={`${customClassPrefix} ${isDarkMode && 'dark-theme'} ${'react-select'}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Input } from '../Input';
|
|||
import { HelperMessage, ValidationMessage } from '../InputUtils/InputUtils';
|
||||
import Tooltip from '../../Tooltip/Tooltip';
|
||||
|
||||
const EditableTitleInput = ({ size, disabled, helperText, onChange, ...restProps }) => {
|
||||
const EditableTitleInput = ({ size, disabled, helperText, onChange: change, readOnly, placeholder, ...restProps }) => {
|
||||
const inputRef = useRef(null);
|
||||
const [tooltipWidth, setTooltipWidth] = useState('auto');
|
||||
const [isValid, setIsValid] = useState(null);
|
||||
|
|
@ -17,7 +17,7 @@ const EditableTitleInput = ({ size, disabled, helperText, onChange, ...restProps
|
|||
setIsValid(validateObj.valid);
|
||||
setMessage(validateObj.message);
|
||||
}
|
||||
onChange(e, validateObj);
|
||||
change(e, validateObj);
|
||||
};
|
||||
|
||||
const inputStyle = `tw-border-transparent hover:tw-border-border-default tw-font-medium tw-pl-[12px] tw-pr-[12px] ${
|
||||
|
|
@ -35,7 +35,14 @@ const EditableTitleInput = ({ size, disabled, helperText, onChange, ...restProps
|
|||
return (
|
||||
<div className="tw-relative">
|
||||
<div className="tw-peer tw-relative" ref={inputRef}>
|
||||
<Input size={size} disabled={disabled} onChange={handleChange} {...restProps} className={inputStyle} />
|
||||
<Input
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
placeholder={disabled && readOnly ? readOnly : placeholder}
|
||||
onChange={handleChange}
|
||||
{...restProps}
|
||||
className={inputStyle}
|
||||
/>
|
||||
<SolidIcon
|
||||
name="editable"
|
||||
width="16px"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const Input = React.forwardRef(({ className, size, type, multiline, response, ro
|
|||
type={isPasswordField && isPasswordVisible ? 'text' : type}
|
||||
className={cn(
|
||||
inputVariants({ size }),
|
||||
`tw-relative tw-peer tw-flex tw-text-[12px]/[18px] tw-w-full tw-rounded-[8px] tw-border-[1px] tw-border-solid tw-bg-background-surface-layer-01 tw-py-[7px] tw-text-text-default focus-visible:tw-ring-[1px] focus-visible:tw-ring-offset-[1px] focus-visible:tw-ring-border-accent-strong focus-visible:tw-ring-offset-border-accent-strong focus-visible:tw-border-transparent disabled:tw-cursor-not-allowed ${props.styles}`,
|
||||
`tw-peer tw-flex tw-text-[12px]/[18px] tw-w-full tw-rounded-[8px] tw-border-[1px] tw-border-solid tw-bg-background-surface-layer-01 tw-py-[7px] tw-text-text-default focus-visible:tw-ring-[1px] focus-visible:tw-ring-offset-[1px] focus-visible:tw-ring-border-accent-strong focus-visible:tw-ring-offset-border-accent-strong focus-visible:tw-border-transparent disabled:tw-cursor-not-allowed ${props.styles}`,
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.12.0
|
||||
3.13.0
|
||||
|
|
|
|||
66
server/data-migrations/1740401100000-SetDefaultWorkspace.ts
Normal file
66
server/data-migrations/1740401100000-SetDefaultWorkspace.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { TOOLJET_EDITIONS } from '@modules/app/constants';
|
||||
import { getCustomEnvVars, getTooljetEdition } from '@helpers/utils.helper';
|
||||
import { Organization } from '@entities/organization.entity';
|
||||
import { WORKSPACE_STATUS } from '@modules/users/constants/lifecycle';
|
||||
|
||||
export class SetDefaultWorkspace1740401100000 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
if (getTooljetEdition() !== TOOLJET_EDITIONS.EE) {
|
||||
console.log('Skipping migration as it is not EE edition');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if default workspace URL is configured
|
||||
const defaultWorkspaceUrl = getCustomEnvVars('TOOLJET_DEFAULT_WORKSPACE_URL');
|
||||
if (defaultWorkspaceUrl) {
|
||||
try {
|
||||
const url = new URL(defaultWorkspaceUrl);
|
||||
const pathParts = url.pathname.split('/');
|
||||
const workspaceSlug = pathParts[pathParts.length - 1];
|
||||
if (workspaceSlug) {
|
||||
const organization = await queryRunner.manager.findOne(Organization, {
|
||||
where: { slug: workspaceSlug, status: WORKSPACE_STATUS.ACTIVE },
|
||||
select: ['id'],
|
||||
});
|
||||
if (organization){
|
||||
await queryRunner.query(`
|
||||
UPDATE organizations
|
||||
SET is_default = true
|
||||
WHERE slug = $1
|
||||
`, [workspaceSlug]);
|
||||
return;
|
||||
}
|
||||
console.log(`No active organization found with slug: ${workspaceSlug}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Invalid TOOLJET_DEFAULT_WORKSPACE_URL format');
|
||||
}
|
||||
}
|
||||
|
||||
// Set the first created organization as default
|
||||
await queryRunner.query(`
|
||||
UPDATE organizations
|
||||
SET is_default = true
|
||||
WHERE id = (
|
||||
SELECT id
|
||||
FROM organizations
|
||||
WHERE status = '${WORKSPACE_STATUS.ACTIVE}'
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 1
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
if (getTooljetEdition() !== TOOLJET_EDITIONS.EE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unset all default workspaces
|
||||
await queryRunner.query(`
|
||||
UPDATE organizations
|
||||
SET is_default = false;
|
||||
`);
|
||||
}
|
||||
}
|
||||
81
server/data-migrations/1742369436314-StepsV2Migration.ts
Normal file
81
server/data-migrations/1742369436314-StepsV2Migration.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { Component } from '@entities/component.entity';
|
||||
import { EntityManager, MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { processDataInBatches } from '@helpers/migration.helper';
|
||||
|
||||
export class StepsV2Migration1742369436314 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const componentTypes = ['Steps'];
|
||||
const batchSize = 100;
|
||||
const entityManager = queryRunner.manager;
|
||||
|
||||
for (const componentType of componentTypes) {
|
||||
await processDataInBatches(
|
||||
entityManager,
|
||||
async (entityManager: EntityManager) => {
|
||||
return await entityManager.find(Component, {
|
||||
where: { type: componentType },
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
},
|
||||
async (entityManager: EntityManager, components: Component[]) => {
|
||||
await this.processUpdates(entityManager, components);
|
||||
},
|
||||
batchSize
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
|
||||
private async processUpdates(entityManager, components) {
|
||||
for (const component of components) {
|
||||
const properties = component.properties;
|
||||
const styles = component.styles;
|
||||
const general = component.general;
|
||||
const generalStyles = component.generalStyles;
|
||||
const validation = component.validation;
|
||||
|
||||
if (styles.visibility) {
|
||||
properties.visibility = styles.visibility;
|
||||
delete styles.visibility;
|
||||
}
|
||||
if (styles.theme) {
|
||||
properties['variant'] = styles.theme;
|
||||
delete styles.theme;
|
||||
}
|
||||
if (styles.color) {
|
||||
styles['completedAccent'] = styles.color;
|
||||
}
|
||||
delete styles.color;
|
||||
if (styles.textColor) {
|
||||
styles['completedLabel'] = styles.textColor;
|
||||
styles['incompletedLabel'] = styles.textColor;
|
||||
styles['currentStepLabel'] = styles.textColor;
|
||||
}
|
||||
delete styles.textColor;
|
||||
if (properties.steps) {
|
||||
properties['schema'] = properties.steps;
|
||||
delete properties.steps;
|
||||
properties['advanced'] = { value: '{{true}}' };
|
||||
}
|
||||
|
||||
// if (properties.stepsSelectable) {
|
||||
// properties.disabledState = styles.disabledState;
|
||||
// delete styles.disabledState;
|
||||
// }
|
||||
|
||||
// if (generalStyles?.boxShadow) {
|
||||
// styles.boxShadow = generalStyles?.boxShadow;
|
||||
// delete generalStyles?.boxShadow;
|
||||
// }
|
||||
|
||||
await entityManager.update(Component, component.id, {
|
||||
properties,
|
||||
styles,
|
||||
general,
|
||||
generalStyles,
|
||||
validation,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit b9e73f87b9062e06c49c2c73add6b82ba21dcacf
|
||||
Subproject commit 30dbfa754562d00f8d64181d5006e113798bd668
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||
|
||||
export class AddIsDefaultToOrganizations1740401000000 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Add is_default column
|
||||
await queryRunner.addColumn(
|
||||
'organizations',
|
||||
new TableColumn({
|
||||
name: 'is_default',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
isNullable: false,
|
||||
})
|
||||
);
|
||||
|
||||
// Create a partial unique index to ensure only one default workspace
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX idx_organizations_single_default
|
||||
ON organizations (is_default)
|
||||
WHERE is_default = true;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// Drop the unique index first
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS idx_organizations_single_default;`);
|
||||
// Then drop the column
|
||||
await queryRunner.dropColumn('organizations', 'is_default');
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import Ajv from 'ajv';
|
|||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { ImportResourcesDto } from '@dto/import-resources.dto';
|
||||
import { AppImportRequestDto } from '@modules/external-apis/dto';
|
||||
|
||||
const ajv = new Ajv({ allErrors: true, coerceTypes: true });
|
||||
const logger = new Logger('TooljetDatabaseSchemaValidator');
|
||||
|
|
@ -109,3 +110,15 @@ export function ValidateTooljetDatabaseSchema(validationOptions?: ValidationOpti
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function ValidateTooljetDatabaseImportSchema(validationOptions?: ValidationOptions) {
|
||||
return function (object: AppImportRequestDto, propertyName: string) {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: ValidateTooljetDatabaseConstraint,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ export class Organization extends BaseEntity {
|
|||
@Column({ name: 'domain' })
|
||||
domain: string;
|
||||
|
||||
@Column({ name: 'is_default', default: false })
|
||||
isDefault: boolean;
|
||||
|
||||
@Column({ name: 'enable_sign_up' })
|
||||
enableSignUp: boolean;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { isEmpty } from 'lodash';
|
|||
import { USER_TYPE } from '@modules/users/constants/lifecycle';
|
||||
import { ConflictException } from '@nestjs/common';
|
||||
import { DataBaseConstraints } from './db_constraints.constants';
|
||||
import { getEnvVars } from 'scripts/database-config-utils';
|
||||
|
||||
|
||||
const semver = require('semver');
|
||||
|
||||
|
|
@ -449,5 +451,11 @@ export const getSubpath = () => {
|
|||
};
|
||||
|
||||
export function getTooljetEdition(): string {
|
||||
return process.env.TOOLJET_EDITION?.toLowerCase() || 'ce';
|
||||
const envVars = getEnvVars();
|
||||
return envVars['TOOLJET_EDITION']?.toLowerCase() || 'ce';
|
||||
}
|
||||
|
||||
export function getCustomEnvVars(name: string) {
|
||||
const envVars = getEnvVars();
|
||||
return envVars[name] || '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { AppsSubscriber } from './subscribers/apps.subscriber';
|
|||
import { AiModule } from '@modules/ai/module';
|
||||
import { AppPermissionsModule } from '@modules/app-permissions/module';
|
||||
import { RolesRepository } from '@modules/roles/repository';
|
||||
import { UsersModule } from '@modules/users/module';
|
||||
@Module({})
|
||||
export class AppsModule {
|
||||
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||
|
|
@ -55,6 +56,7 @@ export class AppsModule {
|
|||
await DataSourcesModule.register(configs),
|
||||
await AiModule.register(configs),
|
||||
await AppPermissionsModule.register(configs),
|
||||
await UsersModule.register(configs),
|
||||
],
|
||||
controllers: [AppsController],
|
||||
providers: [
|
||||
|
|
@ -74,7 +76,7 @@ export class AppsModule {
|
|||
AppImportExportService,
|
||||
RolesRepository,
|
||||
],
|
||||
exports: [AppsUtilService],
|
||||
exports: [AppsUtilService, AppImportExportService],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { App } from '@entities/app.entity';
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource, Repository } from 'typeorm';
|
||||
import { SessionAppData } from './types';
|
||||
import { WorkspaceAppsResponseDto } from '@modules/external-apis/dto';
|
||||
|
||||
@Injectable()
|
||||
export class AppsRepository extends Repository<App> {
|
||||
|
|
@ -63,4 +64,23 @@ export class AppsRepository extends Repository<App> {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findAllOrganizationApps(organizationId: string): Promise<WorkspaceAppsResponseDto[]> {
|
||||
return await this.createQueryBuilder('app')
|
||||
.select([
|
||||
'app.id AS id',
|
||||
'app.name AS name',
|
||||
'app.slug AS slug',
|
||||
'app.created_at AS createdAt',
|
||||
'app.organization_id AS organizationId',
|
||||
'version.id AS versionId',
|
||||
'version.name AS versionName',
|
||||
'version.created_at AS versionCreatedAt',
|
||||
])
|
||||
.leftJoin('app_versions', 'version', 'version.app_id = app.id')
|
||||
.where('app.organizationId = :organizationId', { organizationId })
|
||||
.orderBy('app.created_At', 'ASC')
|
||||
.orderBy('version.created_at', 'ASC')
|
||||
.getRawMany();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,6 @@ import { VersionRepository } from '@modules/versions/repository';
|
|||
import { AppsRepository } from './repository';
|
||||
import { FoldersUtilService } from '@modules/folders/util.service';
|
||||
import { FolderAppsUtilService } from '@modules/folder-apps/util.service';
|
||||
import { DataQuery } from '@entities/data_query.entity';
|
||||
import { DataSource } from '@entities/data_source.entity';
|
||||
import { AppVersion } from '@entities/app_version.entity';
|
||||
import { PageService } from './services/page.service';
|
||||
import { EventsService } from './services/event.service';
|
||||
import { LICENSE_FIELD } from '@modules/licensing/constants';
|
||||
|
|
@ -224,40 +221,7 @@ export class AppsService implements IAppsService {
|
|||
}
|
||||
|
||||
async findTooljetDbTables(appId: string): Promise<{ table_id: string }[]> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const tooljetDbDataQueries = await manager
|
||||
.createQueryBuilder(DataQuery, 'data_queries')
|
||||
.innerJoin(DataSource, 'data_sources', 'data_queries.data_source_id = data_sources.id')
|
||||
.innerJoin(AppVersion, 'app_versions', 'app_versions.id = data_sources.app_version_id')
|
||||
.where('app_versions.app_id = :appId', { appId })
|
||||
.andWhere('data_sources.kind = :kind', { kind: 'tooljetdb' })
|
||||
.getMany();
|
||||
|
||||
const uniqTableIds = new Set();
|
||||
tooljetDbDataQueries.forEach((dq) => {
|
||||
if (dq.options?.operation === 'join_tables') {
|
||||
const joinOptions = dq.options?.join_table?.joins ?? [];
|
||||
(joinOptions || []).forEach((join) => {
|
||||
const { table, conditions } = join;
|
||||
if (table) uniqTableIds.add(table);
|
||||
conditions?.conditionsList?.forEach((condition) => {
|
||||
const { leftField, rightField } = condition;
|
||||
if (leftField?.table) {
|
||||
uniqTableIds.add(leftField?.table);
|
||||
}
|
||||
if (rightField?.table) {
|
||||
uniqTableIds.add(rightField?.table);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (dq.options.table_id) uniqTableIds.add(dq.options.table_id);
|
||||
});
|
||||
|
||||
return [...uniqTableIds].map((table_id) => {
|
||||
return { table_id };
|
||||
});
|
||||
});
|
||||
return await this.appsUtilService.findTooljetDbTables(appId); //moved to util
|
||||
}
|
||||
|
||||
async getOne(app: App, user: User): Promise<any> {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import { DataSourcesUtilService } from '@modules/data-sources/util.service';
|
|||
import { DataSourcesRepository } from '@modules/data-sources/repository';
|
||||
import { AppEnvironmentUtilService } from '@modules/app-environments/util.service';
|
||||
import { ComponentsService } from './component.service';
|
||||
import { UsersUtilService } from '@modules/users/util.service';
|
||||
interface AppResourceMappings {
|
||||
defaultDataSourceIdMapping: Record<string, string>;
|
||||
dataQueryMapping: Record<string, string>;
|
||||
|
|
@ -51,7 +52,17 @@ type DefaultDataSourceName =
|
|||
| 'tooljetdbdefault'
|
||||
| 'workflowsdefault';
|
||||
|
||||
type NewRevampedComponent = 'Text' | 'TextInput' | 'PasswordInput' | 'NumberInput' | 'Table' | 'Button' | 'Checkbox' | 'Divider' | 'VerticalDivider' | 'Link';
|
||||
type NewRevampedComponent =
|
||||
| 'Text'
|
||||
| 'TextInput'
|
||||
| 'PasswordInput'
|
||||
| 'NumberInput'
|
||||
| 'Table'
|
||||
| 'Button'
|
||||
| 'Checkbox'
|
||||
| 'Divider'
|
||||
| 'VerticalDivider'
|
||||
| 'Link';
|
||||
|
||||
const DefaultDataSourceNames: DefaultDataSourceName[] = [
|
||||
'restapidefault',
|
||||
|
|
@ -80,9 +91,10 @@ export class AppImportExportService {
|
|||
protected dataSourcesUtilService: DataSourcesUtilService,
|
||||
protected dataSourcesRepository: DataSourcesRepository,
|
||||
protected appEnvironmentUtilService: AppEnvironmentUtilService,
|
||||
protected usersUtilService: UsersUtilService,
|
||||
protected readonly entityManager: EntityManager,
|
||||
protected componentsService: ComponentsService
|
||||
) { }
|
||||
) {}
|
||||
|
||||
async export(user: User, id: string, searchParams: any = {}): Promise<{ appV2: App }> {
|
||||
// https://github.com/typeorm/typeorm/issues/3857
|
||||
|
|
@ -94,7 +106,7 @@ export class AppImportExportService {
|
|||
.createQueryBuilder(App, 'apps')
|
||||
.where('apps.id = :id AND apps.organization_id = :organizationId', {
|
||||
id,
|
||||
organizationId: user.organizationId,
|
||||
organizationId: user?.organizationId,
|
||||
});
|
||||
const appToExport = await queryForAppToExport.getOne();
|
||||
|
||||
|
|
@ -123,7 +135,7 @@ export class AppImportExportService {
|
|||
const appEnvironments = await manager
|
||||
.createQueryBuilder(AppEnvironment, 'app_environments')
|
||||
.where('app_environments.organizationId = :organizationId', {
|
||||
organizationId: user.organizationId,
|
||||
organizationId: user?.organizationId,
|
||||
})
|
||||
.orderBy('app_environments.createdAt', 'ASC')
|
||||
.getMany();
|
||||
|
|
@ -184,13 +196,13 @@ export class AppImportExportService {
|
|||
const components =
|
||||
pages.length > 0
|
||||
? await manager
|
||||
.createQueryBuilder(Component, 'components')
|
||||
.leftJoinAndSelect('components.layouts', 'layouts')
|
||||
.where('components.pageId IN(:...pageId)', {
|
||||
pageId: pages.map((v) => v.id),
|
||||
})
|
||||
.orderBy('components.created_at', 'ASC')
|
||||
.getMany()
|
||||
.createQueryBuilder(Component, 'components')
|
||||
.leftJoinAndSelect('components.layouts', 'layouts')
|
||||
.where('components.pageId IN(:...pageId)', {
|
||||
pageId: pages.map((v) => v.id),
|
||||
})
|
||||
.orderBy('components.created_at', 'ASC')
|
||||
.getMany()
|
||||
: [];
|
||||
|
||||
const events = await manager
|
||||
|
|
@ -340,8 +352,8 @@ export class AppImportExportService {
|
|||
return await catchDbException(async () => {
|
||||
const importedApp = manager.create(App, {
|
||||
name: appParams.name,
|
||||
organizationId: user.organizationId,
|
||||
userId: user.id,
|
||||
organizationId: user?.organizationId,
|
||||
userId: user.id, //fetch super admin user id for EE
|
||||
slug: null,
|
||||
icon: appParams.icon,
|
||||
creationMode: `${isGitApp ? 'GIT' : 'DEFAULT'}`,
|
||||
|
|
@ -762,7 +774,7 @@ export class AppImportExportService {
|
|||
|
||||
const { dataQueryMapping } = await this.createDataQueriesForAppVersion(
|
||||
manager,
|
||||
user.organizationId,
|
||||
user?.organizationId,
|
||||
importingDataQueriesForAppVersion,
|
||||
importingDataSource,
|
||||
dataSourceForAppVersion,
|
||||
|
|
@ -1059,10 +1071,10 @@ export class AppImportExportService {
|
|||
const options =
|
||||
importingDataSource.kind === 'tooljetdb'
|
||||
? this.replaceTooljetDbTableIds(
|
||||
importingQuery.options,
|
||||
externalResourceMappings['tooljet_database'],
|
||||
organizationId
|
||||
)
|
||||
importingQuery.options,
|
||||
externalResourceMappings['tooljet_database'],
|
||||
organizationId
|
||||
)
|
||||
: importingQuery.options;
|
||||
|
||||
const newQuery = manager.create(DataQuery, {
|
||||
|
|
@ -1153,7 +1165,7 @@ export class AppImportExportService {
|
|||
appResourceMappings: AppResourceMappings
|
||||
) {
|
||||
const defaultDataSourceIds = await this.createDefaultDataSourceForVersion(
|
||||
user.organizationId,
|
||||
user?.organizationId,
|
||||
appResourceMappings.appVersionMapping[appVersion.id],
|
||||
DefaultDataSourceKinds,
|
||||
manager
|
||||
|
|
@ -1192,7 +1204,7 @@ export class AppImportExportService {
|
|||
kind: dataSource.kind,
|
||||
type: DataSourceTypes.DEFAULT,
|
||||
scope: 'global',
|
||||
organizationId: user.organizationId,
|
||||
organizationId: user?.organizationId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -1203,7 +1215,7 @@ export class AppImportExportService {
|
|||
kind: dataSource.kind,
|
||||
type: In([DataSourceTypes.DEFAULT, DataSourceTypes.SAMPLE]),
|
||||
scope: 'global',
|
||||
organizationId: user.organizationId,
|
||||
organizationId: user?.organizationId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -1221,7 +1233,7 @@ export class AppImportExportService {
|
|||
|
||||
if (plugin) {
|
||||
const newDataSource = manager.create(DataSource, {
|
||||
organizationId: user.organizationId,
|
||||
organizationId: user?.organizationId,
|
||||
name: dataSource.name,
|
||||
kind: dataSource.kind,
|
||||
type: DataSourceTypes.DEFAULT,
|
||||
|
|
@ -1236,7 +1248,7 @@ export class AppImportExportService {
|
|||
|
||||
const createNewGlobalDs = async (ds: DataSource): Promise<DataSource> => {
|
||||
const newDataSource = manager.create(DataSource, {
|
||||
organizationId: user.organizationId,
|
||||
organizationId: user?.organizationId,
|
||||
name: dataSource.name,
|
||||
kind: dataSource.kind,
|
||||
type: DataSourceTypes.DEFAULT,
|
||||
|
|
@ -1264,7 +1276,7 @@ export class AppImportExportService {
|
|||
) {
|
||||
appResourceMappings = { ...appResourceMappings };
|
||||
const currentOrgEnvironments = await this.appEnvironmentUtilService.getAll(
|
||||
user.organizationId,
|
||||
user?.organizationId,
|
||||
appVersion.appId,
|
||||
manager
|
||||
);
|
||||
|
|
@ -1326,7 +1338,7 @@ export class AppImportExportService {
|
|||
appResourceMappings = { ...appResourceMappings };
|
||||
const { appVersionMapping, appDefaultEnvironmentMapping } = appResourceMappings;
|
||||
const organization: Organization = await manager.findOne(Organization, {
|
||||
where: { id: user.organizationId },
|
||||
where: { id: user?.organizationId },
|
||||
relations: ['appEnvironments'],
|
||||
});
|
||||
let currentEnvironmentId: string;
|
||||
|
|
@ -1545,7 +1557,7 @@ export class AppImportExportService {
|
|||
|
||||
// Create default data sources
|
||||
const defaultDataSourceIds = await this.createDefaultDataSourceForVersion(
|
||||
user.organizationId,
|
||||
user?.organizationId,
|
||||
version.id,
|
||||
DefaultDataSourceKinds,
|
||||
manager
|
||||
|
|
@ -1553,7 +1565,7 @@ export class AppImportExportService {
|
|||
let envIdArray: string[] = [];
|
||||
|
||||
const organization: Organization = await manager.findOne(Organization, {
|
||||
where: { id: user.organizationId },
|
||||
where: { id: user?.organizationId },
|
||||
relations: ['appEnvironments'],
|
||||
});
|
||||
envIdArray = [...organization.appEnvironments.map((env) => env.id)];
|
||||
|
|
@ -1562,7 +1574,7 @@ export class AppImportExportService {
|
|||
await Promise.all(
|
||||
defaultAppEnvironments.map(async (en) => {
|
||||
const env = manager.create(AppEnvironment, {
|
||||
organizationId: user.organizationId,
|
||||
organizationId: user?.organizationId,
|
||||
name: en.name,
|
||||
isDefault: en.isDefault,
|
||||
priority: en.priority,
|
||||
|
|
@ -1627,10 +1639,10 @@ export class AppImportExportService {
|
|||
options:
|
||||
dataSourceId == defaultDataSourceIds['tooljetdb']
|
||||
? this.replaceTooljetDbTableIds(
|
||||
query.options,
|
||||
externalResourceMappings['tooljet_database'],
|
||||
user.organizationId
|
||||
)
|
||||
query.options,
|
||||
externalResourceMappings['tooljet_database'],
|
||||
user?.organizationId
|
||||
)
|
||||
: query.options,
|
||||
});
|
||||
await manager.save(newQuery);
|
||||
|
|
|
|||
|
|
@ -95,7 +95,9 @@ export class ComponentsService implements IComponentsService {
|
|||
if (componentData.type === 'Table' && _.isArray(objValue)) {
|
||||
return srcValue;
|
||||
} else if (
|
||||
(componentData.type === 'DropdownV2' || componentData.type === 'MultiselectV2') &&
|
||||
(componentData.type === 'DropdownV2' ||
|
||||
componentData.type === 'MultiselectV2' ||
|
||||
componentData.type === 'Steps') &&
|
||||
_.isArray(objValue)
|
||||
) {
|
||||
return _.isArray(srcValue) ? srcValue : Object.values(srcValue);
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ export const containerConfig = {
|
|||
displayName: 'Container',
|
||||
description: 'Group components',
|
||||
defaultSize: {
|
||||
width: 10,
|
||||
height: 200,
|
||||
width: 13,
|
||||
height: 480,
|
||||
},
|
||||
component: 'Container',
|
||||
others: {
|
||||
|
|
|
|||
|
|
@ -4,25 +4,38 @@ export const stepsConfig = {
|
|||
description: 'Step-by-step navigation aid',
|
||||
component: 'Steps',
|
||||
properties: {
|
||||
variant: {
|
||||
type: 'switch',
|
||||
displayName: 'Variant',
|
||||
validation: { schema: { type: 'string' }, defaultValue: 'titles' },
|
||||
options: [
|
||||
{ displayName: 'Label', value: 'titles' },
|
||||
{ displayName: 'Number', value: 'numbers' },
|
||||
{ displayName: 'Plain', value: 'plain' },
|
||||
],
|
||||
accordian: 'label',
|
||||
},
|
||||
schema: {
|
||||
type: 'code',
|
||||
displayName: 'Schema',
|
||||
conditionallyRender: {
|
||||
key: 'advanced',
|
||||
value: true,
|
||||
},
|
||||
accordian: 'Options',
|
||||
},
|
||||
steps: {
|
||||
type: 'code',
|
||||
displayName: 'Steps',
|
||||
displayName: '',
|
||||
showLabel: false,
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'array',
|
||||
element: { type: 'object', object: { id: { type: 'number' } } },
|
||||
element: { type: 'object' },
|
||||
},
|
||||
defaultValue: `[{ name: 'step 1'}, {name: 'step 2'}]`,
|
||||
},
|
||||
},
|
||||
currentStep: {
|
||||
type: 'code',
|
||||
displayName: 'Current step',
|
||||
validation: {
|
||||
schema: { type: 'number' },
|
||||
defaultValue: 1,
|
||||
},
|
||||
},
|
||||
stepsSelectable: {
|
||||
type: 'toggle',
|
||||
displayName: 'Steps selectable',
|
||||
|
|
@ -30,6 +43,36 @@ export const stepsConfig = {
|
|||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
section: 'additionalActions',
|
||||
},
|
||||
disabledState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Disable',
|
||||
validation: { schema: { type: 'boolean' } },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
visibility: {
|
||||
type: 'toggle',
|
||||
displayName: 'Visibility',
|
||||
validation: { schema: { type: 'boolean' }, defaultValue: true },
|
||||
section: 'additionalActions',
|
||||
},
|
||||
advanced: {
|
||||
type: 'toggle',
|
||||
displayName: 'Dynamic options',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: true,
|
||||
},
|
||||
accordian: 'Options',
|
||||
},
|
||||
currentStep: {
|
||||
type: 'code',
|
||||
displayName: 'Current step',
|
||||
validation: {
|
||||
schema: { type: 'number' },
|
||||
defaultValue: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultSize: {
|
||||
|
|
@ -40,46 +83,126 @@ export const stepsConfig = {
|
|||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
handle: 'setStep',
|
||||
displayName: 'Set step',
|
||||
params: [
|
||||
{
|
||||
handle: 'option',
|
||||
displayName: 'Option',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
handle: 'setVisibility',
|
||||
displayName: 'Set visibility',
|
||||
params: [{ handle: 'visible', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setDisabled',
|
||||
displayName: 'Set disabled',
|
||||
params: [{ handle: 'disable', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'resetSteps',
|
||||
displayName: 'Reset steps',
|
||||
params: [],
|
||||
},
|
||||
{
|
||||
handle: 'setStepVisible',
|
||||
displayName: 'Set step visible',
|
||||
params: [
|
||||
{
|
||||
handle: 'id',
|
||||
displayName: 'Step id',
|
||||
},
|
||||
{
|
||||
handle: 'visibility',
|
||||
displayName: 'visibility',
|
||||
defaultValue: '{{false}}',
|
||||
type: 'toggle',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
handle: 'setStepDisable',
|
||||
displayName: 'Set step disable',
|
||||
params: [
|
||||
{
|
||||
handle: 'id',
|
||||
displayName: 'Step id',
|
||||
},
|
||||
{
|
||||
handle: 'disabled',
|
||||
displayName: 'disabled',
|
||||
defaultValue: '{{true}}',
|
||||
type: 'toggle',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
events: {
|
||||
onSelect: { displayName: 'On select' },
|
||||
},
|
||||
styles: {
|
||||
color: {
|
||||
incompletedAccent: {
|
||||
type: 'colorSwatches',
|
||||
displayName: 'Color',
|
||||
displayName: 'Incompleted accent',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#CCD1D5',
|
||||
},
|
||||
accordian: 'steps',
|
||||
},
|
||||
incompletedLabel: {
|
||||
type: 'colorSwatches',
|
||||
displayName: 'Incompleted label',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#1B1F24',
|
||||
},
|
||||
accordian: 'steps',
|
||||
},
|
||||
completedAccent: {
|
||||
type: 'colorSwatches',
|
||||
displayName: 'Completed accent',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'var(--primary-brand)',
|
||||
},
|
||||
accordian: 'steps',
|
||||
},
|
||||
textColor: {
|
||||
completedLabel: {
|
||||
type: 'colorSwatches',
|
||||
displayName: 'Text color',
|
||||
displayName: 'Completed label',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#000000',
|
||||
defaultValue: '#1B1F24',
|
||||
},
|
||||
accordian: 'steps',
|
||||
},
|
||||
theme: {
|
||||
type: 'select',
|
||||
displayName: 'Theme',
|
||||
currentStepLabel: {
|
||||
type: 'colorSwatches',
|
||||
displayName: 'Current step label',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#1B1F24',
|
||||
},
|
||||
accordian: 'steps',
|
||||
},
|
||||
padding: {
|
||||
type: 'switch',
|
||||
displayName: 'Padding',
|
||||
validation: {
|
||||
schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] },
|
||||
defaultValue: 'default',
|
||||
},
|
||||
options: [
|
||||
{ name: 'titles', value: 'titles' },
|
||||
{ name: 'numbers', value: 'numbers' },
|
||||
{ name: 'plain', value: 'plain' },
|
||||
{ displayName: 'Default', value: 'default' },
|
||||
{ displayName: 'None', value: 'none' },
|
||||
],
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'titles',
|
||||
},
|
||||
},
|
||||
visibility: {
|
||||
type: 'toggle',
|
||||
displayName: 'Visibility',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: true,
|
||||
},
|
||||
accordian: 'container',
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
|
|
@ -92,17 +215,35 @@ export const stepsConfig = {
|
|||
},
|
||||
properties: {
|
||||
steps: {
|
||||
value: `{{ [{ name: 'step 1', tooltip: 'some tooltip', id: 1},{ name: 'step 2', tooltip: 'some tooltip', id: 2},{ name: 'step 3', tooltip: 'some tooltip', id: 3},{ name: 'step 4', tooltip: 'some tooltip', id: 4},{ name: 'step 5', tooltip: 'some tooltip', id: 5}]}}`,
|
||||
value: [
|
||||
{ name: 'step 1', tooltip: '', id: 1, visible: { value: true }, disabled: { value: false } },
|
||||
{ name: 'step 2', tooltip: '', id: 2, visible: { value: true }, disabled: { value: false } },
|
||||
{ name: 'step 3', tooltip: '', id: 3, visible: { value: true }, disabled: { value: false } },
|
||||
{ name: 'step 4', tooltip: '', id: 4, visible: { value: true }, disabled: { value: false } },
|
||||
{ name: 'step 5', tooltip: '', id: 5, visible: { value: true }, disabled: { value: false } },
|
||||
],
|
||||
},
|
||||
schema: {
|
||||
value: `{{ [{ name: 'step 1', tooltip: '', id: 1,visible: true, disabled: false},{ name: 'step 2', tooltip: '', id: 2,visible: true, disabled: false},{ name: 'step 3', tooltip: '', id: 3,visible: true, disabled: false},{ name: 'step 4', tooltip: '', id: 4,visible: true, disabled: false},{ name: 'step 5', tooltip: '', id: 5,visible: true, disabled: false}]}}`,
|
||||
},
|
||||
disabledState: { value: '{{false}}' },
|
||||
variant: { value: 'titles' },
|
||||
currentStep: { value: '{{3}}' },
|
||||
stepsSelectable: { value: true },
|
||||
advanced: { value: `{{false}}` },
|
||||
visibility: { value: '{{true}}' },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
theme: { value: 'titles' },
|
||||
color: { value: 'var(--primary-brand)' },
|
||||
textColor: { value: '' },
|
||||
// color: { value: '' },
|
||||
// textColor: { value: '' },
|
||||
padding: { value: 'default' },
|
||||
incompletedAccent: { value: '#E4E7EB' },
|
||||
incompletedLabel: { value: '#1B1F24' },
|
||||
completedAccent: { value: '#4368E3' },
|
||||
completedLabel: { value: '#1B1F24' },
|
||||
currentStepLabel: { value: '#1B1F24' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ import { DataSourcesRepository } from '@modules/data-sources/repository';
|
|||
import { IAppsUtilService } from './interfaces/IUtilService';
|
||||
import { DataSourcesUtilService } from '@modules/data-sources/util.service';
|
||||
import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
|
||||
import { WorkspaceAppsResponseDto } from '@modules/external-apis/dto';
|
||||
import { DataQuery } from '@entities/data_query.entity';
|
||||
import { DataSource } from '@entities/data_source.entity';
|
||||
|
||||
@Injectable()
|
||||
export class AppsUtilService implements IAppsUtilService {
|
||||
|
|
@ -487,7 +490,7 @@ export class AppsUtilService implements IAppsUtilService {
|
|||
if (['Table'].includes(currentComponentData?.component?.component) && isArray(objValue)) {
|
||||
return srcValue;
|
||||
} else if (
|
||||
['DropdownV2', 'MultiselectV2'].includes(currentComponentData?.component?.component) &&
|
||||
['DropdownV2', 'MultiselectV2', 'Steps'].includes(currentComponentData?.component?.component) &&
|
||||
isArray(objValue)
|
||||
) {
|
||||
return isArray(srcValue) ? srcValue : Object.values(srcValue);
|
||||
|
|
@ -522,4 +525,45 @@ export class AppsUtilService implements IAppsUtilService {
|
|||
|
||||
return components;
|
||||
}
|
||||
|
||||
async findAllOrganizationApps(organizationId: string): Promise<WorkspaceAppsResponseDto[]> {
|
||||
return await this.appRepository.findAllOrganizationApps(organizationId);
|
||||
}
|
||||
|
||||
async findTooljetDbTables(appId: string): Promise<{ table_id: string }[]> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const tooljetDbDataQueries = await manager
|
||||
.createQueryBuilder(DataQuery, 'data_queries')
|
||||
.innerJoin(DataSource, 'data_sources', 'data_queries.data_source_id = data_sources.id')
|
||||
.innerJoin(AppVersion, 'app_versions', 'app_versions.id = data_sources.app_version_id')
|
||||
.where('app_versions.app_id = :appId', { appId })
|
||||
.andWhere('data_sources.kind = :kind', { kind: 'tooljetdb' })
|
||||
.getMany();
|
||||
|
||||
const uniqTableIds = new Set();
|
||||
tooljetDbDataQueries.forEach((dq) => {
|
||||
if (dq.options?.operation === 'join_tables') {
|
||||
const joinOptions = dq.options?.join_table?.joins ?? [];
|
||||
(joinOptions || []).forEach((join) => {
|
||||
const { table, conditions } = join;
|
||||
if (table) uniqTableIds.add(table);
|
||||
conditions?.conditionsList?.forEach((condition) => {
|
||||
const { leftField, rightField } = condition;
|
||||
if (leftField?.table) {
|
||||
uniqTableIds.add(leftField?.table);
|
||||
}
|
||||
if (rightField?.table) {
|
||||
uniqTableIds.add(rightField?.table);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (dq.options.table_id) uniqTableIds.add(dq.options.table_id);
|
||||
});
|
||||
|
||||
return [...uniqTableIds].map((table_id) => {
|
||||
return { table_id };
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export class ExternalApiSecurityGuard implements CanActivate {
|
|||
throw new ForbiddenException('External API is disabled');
|
||||
}
|
||||
|
||||
// Check the authorization header
|
||||
// // Check the authorization header
|
||||
const authHeader = request.headers['authorization'];
|
||||
const externalApiAccessToken = this.configService.get<string>('EXTERNAL_API_ACCESS_TOKEN');
|
||||
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ export class OauthService implements IOAuthService {
|
|||
|
||||
// Not logging in to specific organization, creating new
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
defaultOrganization = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
|
||||
defaultOrganization = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
|
||||
|
||||
userDetails = await this.userRepository.createOrUpdate(
|
||||
{
|
||||
|
|
@ -221,7 +221,7 @@ export class OauthService implements IOAuthService {
|
|||
if (!isInviteRedirect) {
|
||||
// no SSO login enabled organization available for user - creating new one
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
organizationDetails = await this.setupOrganizationsUtilService.create(name, slug, userDetails, manager);
|
||||
organizationDetails = await this.setupOrganizationsUtilService.create({ name, slug }, userDetails, manager);
|
||||
await this.userRepository.updateOne(
|
||||
userDetails.id,
|
||||
{ defaultOrganizationId: organizationDetails.id },
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export class AuthService implements IAuthService {
|
|||
} else if (allowPersonalWorkspace && !isInviteRedirect) {
|
||||
// no form login enabled organization available for user - creating new one
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
organization = await this.setupOrganizationsUtilService.create(name, slug, user, manager);
|
||||
organization = await this.setupOrganizationsUtilService.create({ name, slug }, user, manager);
|
||||
} else {
|
||||
if (!isInviteRedirect) throw new UnauthorizedException('User is not assigned to any workspaces');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ export class AuthUtilService implements IAuthUtilService {
|
|||
|
||||
if (!user && allowPersonalWorkspace) {
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
defaultOrganization = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
|
||||
defaultOrganization = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
|
||||
}
|
||||
|
||||
const { source, status } = getUserStatusAndSource(lifecycleEvents.USER_SSO_ACTIVATE, sso);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class DataQueriesService implements IDataQueriesService {
|
|||
protected readonly dataQueryRepository: DataQueryRepository,
|
||||
protected readonly dataQueryUtilService: DataQueriesUtilService,
|
||||
protected readonly dataSourceRepository: DataSourcesRepository
|
||||
) {}
|
||||
) { }
|
||||
|
||||
async getAll(versionId: string) {
|
||||
const queries = await this.dataQueryRepository.getAll(versionId);
|
||||
|
|
@ -30,9 +30,6 @@ export class DataQueriesService implements IDataQueriesService {
|
|||
|
||||
// serialize
|
||||
for (const query of queries) {
|
||||
if (query.dataSource.type === DataSourceTypes.STATIC) {
|
||||
delete query['dataSourceId'];
|
||||
}
|
||||
delete query['dataSource'];
|
||||
|
||||
const decamelizeQuery = decamelizeKeys(query);
|
||||
|
|
|
|||
|
|
@ -37,5 +37,17 @@ export const FEATURES: FeaturesConfig = {
|
|||
license: LICENSE_FIELD.EXTERNAL_API,
|
||||
isPublic: true,
|
||||
},
|
||||
[FEATURE_KEY.GET_ALL_WORKSPACE_APPS]: {
|
||||
license: LICENSE_FIELD.EXTERNAL_API,
|
||||
isPublic: true,
|
||||
},
|
||||
[FEATURE_KEY.IMPORT_APP]: {
|
||||
license: LICENSE_FIELD.EXTERNAL_API,
|
||||
isPublic: true,
|
||||
},
|
||||
[FEATURE_KEY.EXPORT_APP]: {
|
||||
license: LICENSE_FIELD.EXTERNAL_API,
|
||||
isPublic: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,4 +7,41 @@ export enum FEATURE_KEY {
|
|||
UPDATE_USER_WORKSPACE = 'UPDATE_USER_WORKSPACE',
|
||||
GET_ALL_WORKSPACES = 'GET_ALL_WORKSPACES',
|
||||
UPDATE_USER_ROLE = 'UPDATE_USER_ROLE',
|
||||
GET_ALL_WORKSPACE_APPS = 'GET_ALL_WORKSPACE_APPS',
|
||||
IMPORT_APP = 'IMPORT_APP',
|
||||
EXPORT_APP = 'EXPORT_APP',
|
||||
}
|
||||
|
||||
export type DefaultDataSourceKind = 'restapi' | 'runjs' | 'runpy' | 'tooljetdb' | 'workflows';
|
||||
export type NewRevampedComponent =
|
||||
| 'Text'
|
||||
| 'TextInput'
|
||||
| 'PasswordInput'
|
||||
| 'NumberInput'
|
||||
| 'Table'
|
||||
| 'Button'
|
||||
| 'Checkbox';
|
||||
export type DefaultDataSourceName =
|
||||
| 'restapidefault'
|
||||
| 'runjsdefault'
|
||||
| 'runpydefault'
|
||||
| 'tooljetdbdefault'
|
||||
| 'workflowsdefault';
|
||||
|
||||
export const DefaultDataSourceKinds: DefaultDataSourceKind[] = ['restapi', 'runjs', 'runpy', 'tooljetdb', 'workflows'];
|
||||
export const DefaultDataSourceNames: DefaultDataSourceName[] = [
|
||||
'restapidefault',
|
||||
'runjsdefault',
|
||||
'runpydefault',
|
||||
'tooljetdbdefault',
|
||||
'workflowsdefault',
|
||||
];
|
||||
export const NewRevampedComponents: NewRevampedComponent[] = [
|
||||
'Text',
|
||||
'TextInput',
|
||||
'PasswordInput',
|
||||
'NumberInput',
|
||||
'Table',
|
||||
'Checkbox',
|
||||
'Button',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Controller, Get, Param, UseGuards, Body, Patch, Post, Put, NotFoundException } from '@nestjs/common';
|
||||
import { ExternalApiSecurityGuard } from './guards/external-api-security.guard';
|
||||
import { UpdateUserDto, WorkspaceDto, UpdateGivenWorkspaceDto, CreateUserDto } from './dto';
|
||||
import { IExternalApisController } from './Interfaces/IController';
|
||||
import { EditUserRoleDto } from '@modules/roles/dto';
|
||||
import { ExternalApiSecurityGuard } from '@modules/auth/guards/external-api-security.guard';
|
||||
|
||||
@Controller('ext')
|
||||
export class ExternalApisController implements IExternalApisController {
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ import {
|
|||
MaxLength,
|
||||
ValidateIf,
|
||||
IsNotEmpty,
|
||||
IsDefined,
|
||||
IsObject,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { USER_ROLE } from '@modules/group-permissions/constants';
|
||||
|
||||
import { TjdbSchemaToLatestVersion } from '@dto/transformers/resource-transformer';
|
||||
import { ValidateTooljetDatabaseImportSchema } from '@dto/validators/tooljet-database.validator';
|
||||
export enum Status {
|
||||
ACTIVE = 'active',
|
||||
ARCHIVED = 'archived',
|
||||
|
|
@ -131,3 +134,73 @@ export class UpdateUserWorkspaceDto {
|
|||
@IsOptional()
|
||||
groups?: GroupDto[];
|
||||
}
|
||||
|
||||
export class VersionDto {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
export class AppWithVersionsDto {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
createdAt: Date;
|
||||
organizationId: string;
|
||||
versions: VersionDto[];
|
||||
versionCount: number;
|
||||
}
|
||||
|
||||
export class WorkspaceAppsResponseDto {
|
||||
apps: AppWithVersionsDto[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export class AppImportRequestDto {
|
||||
@IsString()
|
||||
tooljet_version: string;
|
||||
|
||||
// TODO: Add transformation and validation for app similar to tooljet_database
|
||||
@IsOptional()
|
||||
app: AppImportDto[];
|
||||
|
||||
// Optional parameter -> To be provided in import request to import app with custom name.
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
appName: string;
|
||||
|
||||
// TJ-DB field
|
||||
@IsOptional()
|
||||
// Transform the input data to the latest schema version
|
||||
// This should be applied first to ensure the data is in
|
||||
// the correct format before validation
|
||||
@Transform(TjdbSchemaToLatestVersion)
|
||||
@ValidateNested({ each: true })
|
||||
// Ensure each item is properly instantiated as ImportTooljetDatabaseDto
|
||||
// This is crucial for nested validation to work correctly
|
||||
@Type(() => ImportTooljetDatabaseDto)
|
||||
// Custom validator to check against the tooljet database schema
|
||||
// This should be applied last to validate the transformed
|
||||
// and instantiated data
|
||||
@ValidateTooljetDatabaseImportSchema({ each: true })
|
||||
tooljet_database: ImportTooljetDatabaseDto[];
|
||||
}
|
||||
export class AppImportDto {
|
||||
@IsDefined()
|
||||
@IsObject()
|
||||
definition: any;
|
||||
}
|
||||
|
||||
export class ImportTooljetDatabaseDto {
|
||||
@IsUUID()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
table_name: string;
|
||||
|
||||
@IsDefined()
|
||||
schema: any;
|
||||
|
||||
// @IsOptional()
|
||||
// data: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class ExternalApiSecurityGuard implements CanActivate {
|
||||
constructor(protected configService: ConfigService) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
// Check if external API is enabled
|
||||
const isExternalApiEnabled = this.configService.get<string>('ENABLE_EXTERNAL_API') === 'true';
|
||||
if (!isExternalApiEnabled) {
|
||||
throw new ForbiddenException('External API is disabled');
|
||||
}
|
||||
|
||||
// Check the authorization header
|
||||
const authHeader = request.headers['authorization'];
|
||||
const externalApiAccessToken = this.configService.get<string>('EXTERNAL_API_ACCESS_TOKEN');
|
||||
|
||||
if (!authHeader || authHeader !== `Basic ${externalApiAccessToken}`) {
|
||||
throw new ForbiddenException('Unauthorized');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,12 @@ import { GroupPermissionsModule } from '@modules/group-permissions/module';
|
|||
import { RolesModule } from '@modules/roles/module';
|
||||
import { DynamicModule } from '@nestjs/common';
|
||||
import { getImportPath } from '@modules/app/constants';
|
||||
import { ExternalApiSecurityGuard } from './guards/external-api-security.guard';
|
||||
import { RolesRepository } from '@modules/roles/repository';
|
||||
import { TooljetDbModule } from '@modules/tooljet-db/module';
|
||||
import { AppsModule } from '@modules/apps/module';
|
||||
import { OrganizationsModule } from '@modules/organizations/module';
|
||||
import { VersionModule } from '@modules/versions/module';
|
||||
import { UsersModule } from '@modules/users/module';
|
||||
export class ExternalApiModule {
|
||||
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||
const importPath = await getImportPath(configs?.IS_GET_CONTEXT);
|
||||
|
|
@ -14,14 +18,16 @@ export class ExternalApiModule {
|
|||
|
||||
return {
|
||||
module: ExternalApiModule,
|
||||
imports: [await RolesModule.register(configs), await GroupPermissionsModule.register(configs)],
|
||||
providers: [
|
||||
ExternalApiUtilService,
|
||||
ExternalApisService,
|
||||
ExternalApiSecurityGuard,
|
||||
FeatureAbilityFactory,
|
||||
RolesRepository,
|
||||
imports: [
|
||||
await UsersModule.register(configs),
|
||||
await RolesModule.register(configs),
|
||||
await GroupPermissionsModule.register(configs),
|
||||
await TooljetDbModule.register(configs),
|
||||
await AppsModule.register(configs),
|
||||
await OrganizationsModule.register(configs),
|
||||
await VersionModule.register(configs),
|
||||
],
|
||||
providers: [ExternalApiUtilService, ExternalApisService, FeatureAbilityFactory, RolesRepository],
|
||||
controllers: [ExternalApisController],
|
||||
exports: [ExternalApiUtilService],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ interface Features {
|
|||
[FEATURE_KEY.UPDATE_USER_WORKSPACE]: FeatureConfig;
|
||||
[FEATURE_KEY.GET_ALL_WORKSPACES]: FeatureConfig;
|
||||
[FEATURE_KEY.UPDATE_USER_ROLE]: FeatureConfig;
|
||||
[FEATURE_KEY.GET_ALL_WORKSPACE_APPS]: FeatureConfig;
|
||||
[FEATURE_KEY.IMPORT_APP]: FeatureConfig;
|
||||
[FEATURE_KEY.EXPORT_APP]: FeatureConfig;
|
||||
}
|
||||
|
||||
export interface FeaturesConfig {
|
||||
|
|
@ -22,3 +25,13 @@ export interface ValidateEditUserGroupAdditionObject {
|
|||
groupsToAddIds: string[];
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export interface AppResourceMappings {
|
||||
defaultDataSourceIdMapping: Record<string, string>;
|
||||
dataQueryMapping: Record<string, string>;
|
||||
appVersionMapping: Record<string, string>;
|
||||
appEnvironmentMapping: Record<string, string>;
|
||||
appDefaultEnvironmentMapping: Record<string, string[]>;
|
||||
pagesMapping: Record<string, string>;
|
||||
componentsMapping: Record<string, string>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export class LoginConfigsService implements ILoginConfigsService {
|
|||
throw new NotFoundException();
|
||||
}
|
||||
if (!organizationId) {
|
||||
const result = this.loginConfigsUtilService.constructSSOConfigs();
|
||||
const result = await this.loginConfigsUtilService.constructSSOConfigs();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ export class OnboardingController implements IOnboardingController {
|
|||
@InitFeature(FEATURE_KEY.SIGNUP)
|
||||
@UseGuards(
|
||||
SignupDisableGuard,
|
||||
AllowPersonalWorkspaceGuard,
|
||||
UserCountGuard,
|
||||
EditorUserCountGuard,
|
||||
FirstUserSignupDisableGuard,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export interface IOnboardingUtilService {
|
|||
signingUpOrganization: Organization,
|
||||
userParams: { firstName: string; lastName: string; password: string },
|
||||
redirectTo?: string,
|
||||
defaultWorkspace?: Organization,
|
||||
manager?: EntityManager
|
||||
): Promise<void>;
|
||||
processOrganizationSignup(
|
||||
|
|
@ -40,4 +41,10 @@ export interface IOnboardingUtilService {
|
|||
organizationInviteUrl: string;
|
||||
}>;
|
||||
splitName(name: string): { firstName: string; lastName: string };
|
||||
updateExistingUserDefaultWorkspace(
|
||||
userParams: { password: string; firstName: string; lastName: string },
|
||||
existingUser: User,
|
||||
defaultWorkspace: Organization,
|
||||
manager?: EntityManager
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,9 @@ export class OnboardingService implements IOnboardingService {
|
|||
const { firstName, lastName } = names;
|
||||
const userParams = { email, password, firstName, lastName };
|
||||
|
||||
// Find the default workspace
|
||||
const defaultWorkspace = await this.organizationRepository. getDefaultWorkspaceOfInstance();
|
||||
|
||||
if (existingUser) {
|
||||
// Handling instance and workspace level signup for existing user
|
||||
return await this.onboardingUtilService.whatIfTheSignUpIsAtTheWorkspaceLevel(
|
||||
|
|
@ -126,9 +129,18 @@ export class OnboardingService implements IOnboardingService {
|
|||
signingUpOrganization,
|
||||
userParams,
|
||||
redirectTo,
|
||||
defaultWorkspace,
|
||||
manager
|
||||
);
|
||||
} else {
|
||||
if(defaultWorkspace && !signingUpOrganization) {
|
||||
return await this.onboardingUtilService.createUserInDefaultWorkspace(
|
||||
userParams,
|
||||
defaultWorkspace,
|
||||
redirectTo,
|
||||
manager
|
||||
);
|
||||
}
|
||||
return await this.onboardingUtilService.createUserOrPersonalWorkspace(
|
||||
userParams,
|
||||
existingUser,
|
||||
|
|
@ -149,8 +161,7 @@ export class OnboardingService implements IOnboardingService {
|
|||
const result = await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
// Create first organization
|
||||
const organization = await this.organizationRepository.createOne(
|
||||
workspace || 'My workspace',
|
||||
'my-workspace',
|
||||
{ name: workspace || 'My workspace', slug: 'my-workspace' },
|
||||
manager
|
||||
);
|
||||
|
||||
|
|
@ -226,7 +237,8 @@ export class OnboardingService implements IOnboardingService {
|
|||
(await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) ===
|
||||
'true';
|
||||
|
||||
if (!(allowPersonalWorkspace || organizationToken)) {
|
||||
const defaultWorkspace = await this.organizationRepository.getDefaultWorkspaceOfInstance();
|
||||
if (!(defaultWorkspace || allowPersonalWorkspace || organizationToken)) {
|
||||
throw new BadRequestException('Invalid invitation link');
|
||||
}
|
||||
if (organizationToken) {
|
||||
|
|
@ -251,7 +263,8 @@ export class OnboardingService implements IOnboardingService {
|
|||
throw new BadRequestException('Please enter password');
|
||||
}
|
||||
|
||||
if (allowPersonalWorkspace) {
|
||||
const activateDefaultWorkspace = (defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId) || allowPersonalWorkspace;
|
||||
if (activateDefaultWorkspace) {
|
||||
// Getting default workspace
|
||||
const defaultOrganizationUser: OrganizationUser = user.organizationUsers.find(
|
||||
(ou) => ou.organizationId === user.defaultOrganizationId
|
||||
|
|
@ -264,6 +277,14 @@ export class OnboardingService implements IOnboardingService {
|
|||
// Activate default workspace
|
||||
await this.organizationUsersUtilService.activateOrganization(defaultOrganizationUser, manager);
|
||||
|
||||
if(defaultWorkspace && defaultWorkspace.id === user.defaultOrganizationId){
|
||||
const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(user.id);
|
||||
for(const personalWorkspace of personalWorkspaces){
|
||||
// if any personal workspace left. activate those
|
||||
await this.organizationUsersUtilService.activateOrganization(personalWorkspace, manager);
|
||||
}
|
||||
}
|
||||
|
||||
if (workspaceName) {
|
||||
const { slug } = generateNextNameAndSlug('My workspace');
|
||||
await this.organizationRepository.updateOne(
|
||||
|
|
@ -449,10 +470,10 @@ export class OnboardingService implements IOnboardingService {
|
|||
onboarding_details: {
|
||||
status: user.onboardingStatus,
|
||||
password: isPasswordMandatory(user.source), // Should accept password if user is setting up first time
|
||||
questions:
|
||||
(this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true' &&
|
||||
!organizationUser) || // Should ask onboarding questions if first user of the instance. If ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=true, then will ask questions to all signup users
|
||||
(await this.userRepository.count({ where: { status: USER_STATUS.ACTIVE } })) === 0,
|
||||
// questions:
|
||||
// (this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true' &&
|
||||
// !organizationUser) || // Should ask onboarding questions if first user of the instance. If ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=true, then will ask questions to all signup users
|
||||
// (await this.userRepository.count({ where: { status: USER_STATUS.ACTIVE } })) === 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -686,8 +707,7 @@ export class OnboardingService implements IOnboardingService {
|
|||
// Create first organization
|
||||
const workspaceSlug = generateWorkspaceSlug(workspaceName || 'My workspace');
|
||||
const organization = await this.setupOrganizationsUtilService.create(
|
||||
workspaceName || 'My workspace',
|
||||
workspaceSlug,
|
||||
{ name: workspaceName || 'My workspace', slug: workspaceSlug },
|
||||
null,
|
||||
manager
|
||||
);
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ export class OnboardingUtilService implements IOnboardingUtilService {
|
|||
signingUpOrganization: Organization,
|
||||
userParams: { firstName: string; lastName: string; password: string },
|
||||
redirectTo?: string,
|
||||
defaultWorkspace?: Organization,
|
||||
manager?: EntityManager
|
||||
) => {
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
|
|
@ -251,19 +252,28 @@ export class OnboardingUtilService implements IOnboardingUtilService {
|
|||
case hasWorkspaceInviteButUserWantsInstanceSignup: {
|
||||
const firstTimeSignup = ![SOURCE.SIGNUP, SOURCE.WORKSPACE_SIGNUP].includes(existingUser.source as SOURCE);
|
||||
if (firstTimeSignup) {
|
||||
if(defaultWorkspace) {
|
||||
return this.updateExistingUserDefaultWorkspace({
|
||||
password,
|
||||
firstName,
|
||||
lastName
|
||||
},existingUser, defaultWorkspace, manager);
|
||||
}
|
||||
|
||||
/* Invite user doing instance signup. So reset name fields and set password */
|
||||
let defaultOrganizationId = existingUser.defaultOrganizationId;
|
||||
const isPersonalWorkspaceAllowed =
|
||||
(await this.instanceSettingsUtilService.getSettings(INSTANCE_USER_SETTINGS.ALLOW_PERSONAL_WORKSPACE)) ===
|
||||
'true';
|
||||
if (!existingUser.defaultOrganizationId && isPersonalWorkspaceAllowed) {
|
||||
|
||||
if (!existingUser.defaultOrganizationId && isPersonalWorkspaceAllowed) {
|
||||
const personalWorkspaces = await this.organizationUsersUtilService.personalWorkspaces(existingUser.id);
|
||||
if (personalWorkspaces.length) {
|
||||
defaultOrganizationId = personalWorkspaces[0].organizationId;
|
||||
} else {
|
||||
/* Create a personal workspace for the user */
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
const defaultOrganization = await this.organizationRepository.createOne(name, slug, manager);
|
||||
const defaultOrganization = await this.organizationRepository.createOne({ name, slug }, manager);
|
||||
defaultOrganizationId = defaultOrganization.id;
|
||||
await this.organizationUserRepository.createOne(existingUser, defaultOrganization, true, manager);
|
||||
}
|
||||
|
|
@ -272,7 +282,6 @@ export class OnboardingUtilService implements IOnboardingUtilService {
|
|||
userId: existingUser.id,
|
||||
});
|
||||
}
|
||||
|
||||
await this.userRepository.updateOne(
|
||||
existingUser.id,
|
||||
{
|
||||
|
|
@ -398,7 +407,7 @@ export class OnboardingUtilService implements IOnboardingUtilService {
|
|||
let personalWorkspace: Organization;
|
||||
if (isPersonalWorkspaceEnabled) {
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
personalWorkspace = await this.setupOrganizationsUtilService.create(name, slug, null, manager);
|
||||
personalWorkspace = await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
|
||||
}
|
||||
const organizationRole = personalWorkspace ? USER_ROLE.ADMIN : USER_ROLE.END_USER;
|
||||
|
||||
|
|
@ -604,4 +613,130 @@ export class OnboardingUtilService implements IOnboardingUtilService {
|
|||
manager
|
||||
);
|
||||
}
|
||||
|
||||
createUserInDefaultWorkspace = async (
|
||||
userParams: { email: string; password: string; firstName: string; lastName: string },
|
||||
defaultWorkspace: Organization,
|
||||
redirectTo?: string,
|
||||
manager?: EntityManager
|
||||
) => {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const { email, password, firstName, lastName } = userParams;
|
||||
|
||||
if (!defaultWorkspace) {
|
||||
throw new Error('No default workspace found in the instance');
|
||||
}
|
||||
|
||||
// Create user with end-user role in default workspace
|
||||
const lifeCycleParms = getUserStatusAndSource(lifecycleEvents.USER_SIGN_UP);
|
||||
|
||||
const user = await this.create(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
...(firstName && { firstName }),
|
||||
...(lastName && { lastName }),
|
||||
...lifeCycleParms,
|
||||
},
|
||||
defaultWorkspace.id,
|
||||
USER_ROLE.END_USER,
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
manager,
|
||||
false
|
||||
);
|
||||
|
||||
// Create organization user entry
|
||||
await this.organizationUserRepository.createOne(
|
||||
user,
|
||||
defaultWorkspace,
|
||||
true,
|
||||
manager,
|
||||
WORKSPACE_USER_SOURCE.SIGNUP
|
||||
);
|
||||
|
||||
// Validate license
|
||||
await this.licenseUserService.validateUser(manager);
|
||||
|
||||
// Send welcome email
|
||||
this.eventEmitter.emit('emailEvent', {
|
||||
type: EMAIL_EVENTS.SEND_WELCOME_EMAIL,
|
||||
payload: {
|
||||
to: user.email,
|
||||
name: user.firstName,
|
||||
invitationtoken: user.invitationToken,
|
||||
},
|
||||
});
|
||||
|
||||
return {};
|
||||
}, manager);
|
||||
};
|
||||
|
||||
updateExistingUserDefaultWorkspace = async (
|
||||
userParams: { password: string; firstName: string; lastName: string },
|
||||
existingUser: User,
|
||||
defaultWorkspace: Organization,
|
||||
manager?: EntityManager
|
||||
) => {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const { password, firstName, lastName } = userParams;
|
||||
// Create organization user entry if not exists
|
||||
const existingOrgUser = await this.organizationUserRepository.findOne({
|
||||
where: {
|
||||
userId: existingUser.id,
|
||||
organizationId: defaultWorkspace.id,
|
||||
}
|
||||
});
|
||||
|
||||
if(existingOrgUser){
|
||||
throw new NotAcceptableException(
|
||||
'The user is already registered. Please check your inbox for the activation link'
|
||||
);
|
||||
}
|
||||
|
||||
// Update user's default organization ID
|
||||
await this.userRepository.updateOne(
|
||||
existingUser.id,
|
||||
{
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
source: SOURCE.SIGNUP,
|
||||
defaultOrganizationId: defaultWorkspace.id,
|
||||
},
|
||||
manager
|
||||
);
|
||||
|
||||
await this.organizationUserRepository.createOne(
|
||||
existingUser,
|
||||
defaultWorkspace,
|
||||
true,
|
||||
manager,
|
||||
WORKSPACE_USER_SOURCE.SIGNUP
|
||||
);
|
||||
|
||||
// Add end-user role in default workspace if not already present
|
||||
await this.rolesUtilService.addUserRole(
|
||||
defaultWorkspace.id,
|
||||
{ role: USER_ROLE.END_USER, userId: existingUser.id },
|
||||
manager
|
||||
);
|
||||
|
||||
// Validate license
|
||||
await this.licenseUserService.validateUser(manager);
|
||||
|
||||
// send welcome email
|
||||
this.eventEmitter.emit('emailEvent', {
|
||||
type: EMAIL_EVENTS.SEND_WELCOME_EMAIL,
|
||||
payload: {
|
||||
to: existingUser.email,
|
||||
name: existingUser.firstName,
|
||||
invitationtoken: existingUser.invitationToken,
|
||||
},
|
||||
});
|
||||
|
||||
return {};
|
||||
}, manager);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,11 +31,11 @@ export const TJDefaultTheme: Definition = {
|
|||
light: '#1B1F24',
|
||||
dark: '#CFD3D8',
|
||||
},
|
||||
secondary: {
|
||||
placeholder: {
|
||||
light: '#6A727C',
|
||||
dark: '#858C94',
|
||||
},
|
||||
tertiary: {
|
||||
disabled: {
|
||||
light: '#ACB2B9',
|
||||
dark: '#545B64',
|
||||
},
|
||||
|
|
@ -48,15 +48,15 @@ export const TJDefaultTheme: Definition = {
|
|||
large: 0,
|
||||
},
|
||||
colors: {
|
||||
primary: {
|
||||
default: {
|
||||
light: '#CCD1D5',
|
||||
dark: '#3C434B',
|
||||
},
|
||||
secondary: {
|
||||
weak: {
|
||||
light: '#E4E7EB',
|
||||
dark: '#EEF0F1',
|
||||
},
|
||||
tertiary: {
|
||||
disabled: {
|
||||
light: '#E4E7EB',
|
||||
dark: '#F6F8FA',
|
||||
},
|
||||
|
|
@ -64,15 +64,15 @@ export const TJDefaultTheme: Definition = {
|
|||
},
|
||||
systemStatus: {
|
||||
colors: {
|
||||
primary: {
|
||||
success: {
|
||||
light: '#1E823B',
|
||||
dark: '#318344',
|
||||
},
|
||||
secondary: {
|
||||
error: {
|
||||
light: '#D72D39',
|
||||
dark: '#D03F43',
|
||||
},
|
||||
tertiary: {
|
||||
warning: {
|
||||
light: '#BF4F03',
|
||||
dark: '#BA5722',
|
||||
},
|
||||
|
|
@ -84,6 +84,18 @@ export const TJDefaultTheme: Definition = {
|
|||
light: '#F6F6F6',
|
||||
dark: '#121518',
|
||||
},
|
||||
surface1: {
|
||||
light: '#FFFFFF',
|
||||
dark: '#1E2226',
|
||||
},
|
||||
surface2: {
|
||||
light: '#F6F8FA',
|
||||
dark: '#2B3036',
|
||||
},
|
||||
surface3: {
|
||||
light: '#E4E7EB',
|
||||
dark: '#3C434B',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ class TextColors {
|
|||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => Color)
|
||||
secondary?: Color;
|
||||
placeholder?: Color;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => Color)
|
||||
tertiary?: Color;
|
||||
disabled?: Color;
|
||||
}
|
||||
|
||||
class Text {
|
||||
|
|
@ -64,17 +64,17 @@ class BorderRadius {
|
|||
class BorderColors {
|
||||
@ValidateNested()
|
||||
@Type(() => Color)
|
||||
primary: Color;
|
||||
default: Color;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => Color)
|
||||
secondary?: Color;
|
||||
weak?: Color;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => Color)
|
||||
tertiary?: Color;
|
||||
disabled?: Color;
|
||||
}
|
||||
|
||||
class Border {
|
||||
|
|
@ -90,17 +90,17 @@ class Border {
|
|||
class SystemStatusColors {
|
||||
@ValidateNested()
|
||||
@Type(() => Color)
|
||||
primary: Color;
|
||||
success: Color;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => Color)
|
||||
secondary?: Color;
|
||||
error?: Color;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => Color)
|
||||
tertiary?: Color;
|
||||
warning?: Color;
|
||||
}
|
||||
|
||||
class SystemStatus {
|
||||
|
|
@ -121,6 +121,18 @@ class SurfaceColors {
|
|||
@ValidateNested()
|
||||
@Type(() => AppBackgroundColor)
|
||||
appBackground: AppBackgroundColor;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Color)
|
||||
surface1: Color;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Color)
|
||||
surface2: Color;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Color)
|
||||
surface3: Color;
|
||||
}
|
||||
|
||||
class Surface {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
lifecycleEvents,
|
||||
USER_STATUS,
|
||||
USER_TYPE,
|
||||
WORKSPACE_USER_SOURCE,
|
||||
WORKSPACE_USER_STATUS,
|
||||
} from '@modules/users/constants/lifecycle';
|
||||
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
|
||||
|
|
@ -212,7 +213,7 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
|
|||
|
||||
async createDefaultOrganization(manager: EntityManager) {
|
||||
const { name, slug } = generateNextNameAndSlug('My workspace');
|
||||
return await this.setupOrganizationsUtilService.create(name, slug, null, manager);
|
||||
return await this.setupOrganizationsUtilService.create({ name, slug }, null, manager);
|
||||
}
|
||||
|
||||
addUserAsAdmin(userId: string, organizationId: string, manager: EntityManager) {
|
||||
|
|
@ -343,7 +344,7 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
|
|||
|
||||
async personalWorkspaces(userId: string): Promise<OrganizationUser[]> {
|
||||
const personalWorkspaces: Partial<OrganizationUser[]> = await this.organizationUsersRepository.find({
|
||||
select: ['organizationId', 'invitationToken'],
|
||||
select: ['organizationId', 'invitationToken', 'id'],
|
||||
where: { userId },
|
||||
});
|
||||
const personalWorkspaceArray: OrganizationUser[] = [];
|
||||
|
|
@ -578,4 +579,41 @@ export class OrganizationUsersUtilService implements IOrganizationUsersUtilServi
|
|||
user.organizationUserSource = organizationUser.source;
|
||||
return user;
|
||||
}
|
||||
|
||||
addUserToWorkspace = async (
|
||||
user: User,
|
||||
workspace: Organization,
|
||||
manager?: EntityManager
|
||||
) => {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
// Create organization user entry if not exists
|
||||
let existingOrgUser = await this.organizationUsersRepository.findOne({
|
||||
where: {
|
||||
userId: user.id,
|
||||
organizationId: workspace.id,
|
||||
}
|
||||
});
|
||||
|
||||
if(existingOrgUser){
|
||||
return existingOrgUser;
|
||||
}
|
||||
|
||||
const organizationUser = await this.organizationUsersRepository.createOne(
|
||||
user,
|
||||
workspace,
|
||||
true,
|
||||
manager,
|
||||
WORKSPACE_USER_SOURCE.SIGNUP
|
||||
);
|
||||
|
||||
// Add end-user role in default workspace if not already present
|
||||
await this.rolesUtilService.addUserRole(
|
||||
workspace.id,
|
||||
{ role: USER_ROLE.END_USER, userId: user.id },
|
||||
manager
|
||||
);
|
||||
|
||||
return organizationUser;
|
||||
}, manager);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
can([FEATURE_KEY.UPDATE, FEATURE_KEY.GET, FEATURE_KEY.CHECK_UNIQUE], Organization);
|
||||
}
|
||||
if (superAdmin) {
|
||||
can([FEATURE_KEY.WORKSPACE_STATUS_UPDATE], Organization);
|
||||
can([FEATURE_KEY.WORKSPACE_STATUS_UPDATE, FEATURE_KEY.SET_DEFAULT], Organization);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,5 +14,6 @@ export const FEATURES: FeaturesConfig = {
|
|||
[FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: {
|
||||
isPublic: true,
|
||||
},
|
||||
[FEATURE_KEY.SET_DEFAULT]: {},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,4 +26,5 @@ export enum FEATURE_KEY {
|
|||
CHECK_UNIQUE = 'check_unique',
|
||||
CREATE = 'create',
|
||||
CHECK_UNIQUE_ONBOARDING = 'check_unique_onboarding',
|
||||
SET_DEFAULT = 'set_default',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Get, Patch, UseGuards, Query, Param } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Patch, UseGuards, Query, Param, NotImplementedException } from '@nestjs/common';
|
||||
import { OrganizationsService } from '@modules/organizations/service';
|
||||
import { decamelizeKeys } from 'humps';
|
||||
import { User } from '@modules/app/decorators/user.decorator';
|
||||
|
|
@ -17,7 +17,7 @@ import { OrganizationAuthGuard } from '@modules/session/guards/organization-auth
|
|||
@Controller('organizations')
|
||||
@InitModule(MODULES.ORGANIZATIONS)
|
||||
export class OrganizationsController implements IOrganizationsController {
|
||||
constructor(private organizationsService: OrganizationsService) {}
|
||||
constructor(protected organizationsService: OrganizationsService) {}
|
||||
|
||||
@InitFeature(FEATURE_KEY.GET)
|
||||
// TODO: Change to jwt auth guard - check why we need OrganizationAuthGuard here
|
||||
|
|
@ -41,6 +41,15 @@ export class OrganizationsController implements IOrganizationsController {
|
|||
await this.organizationsService.updateOrganizationNameAndSlug(user.organizationId, organizationUpdateDto);
|
||||
return;
|
||||
}
|
||||
|
||||
@InitFeature(FEATURE_KEY.SET_DEFAULT)
|
||||
@UseGuards(JwtAuthGuard, FeatureAbilityGuard)
|
||||
@Patch(':id/set-default')
|
||||
async setDefaultWorkspace(@Param('id') id: string) {
|
||||
await this.organizationsService.setDefaultWorkspace(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Note : This endpoint is used for archive/unarchive workspaces.
|
||||
@InitFeature(FEATURE_KEY.WORKSPACE_STATUS_UPDATE)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
|
|
|||
|
|
@ -11,4 +11,6 @@ export interface IOrganizationsController {
|
|||
checkWorkspaceUnique(name: string, slug: string): Promise<void>;
|
||||
|
||||
checkUniqueWorkspaceName(name: string): Promise<void>;
|
||||
|
||||
setDefaultWorkspace(id: string): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { OrganizationUpdateDto, OrganizationStatusUpdateDto } from '@modules/organizations/dto';
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
export interface IOrganizationsService {
|
||||
fetchOrganizations(
|
||||
|
|
@ -15,4 +16,8 @@ export interface IOrganizationsService {
|
|||
updateOrganizationStatus(organizationId: string, updatableData: OrganizationStatusUpdateDto): Promise<Organization>;
|
||||
|
||||
checkWorkspaceUniqueness(name: string, slug: string): Promise<void>;
|
||||
|
||||
checkWorkspaceNameUniqueness(name: string): Promise<void>;
|
||||
|
||||
setDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise<void>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export interface IOrganizationUtilService {
|
||||
validateWorkspaceExists(workspaceId: string): Promise<void>;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { DynamicModule } from '@nestjs/common';
|
|||
import { getImportPath } from '@modules/app/constants';
|
||||
import { InstanceSettingsModule } from '@modules/instance-settings/module';
|
||||
import { OrganizationRepository } from './repository';
|
||||
import { AppEnvironmentsModule } from '@modules/app-environments/module';
|
||||
|
||||
export class OrganizationsModule {
|
||||
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||
|
|
@ -9,13 +10,14 @@ export class OrganizationsModule {
|
|||
const { OrganizationsService } = await import(`${importPath}/organizations/service`);
|
||||
const { OrganizationsController } = await import(`${importPath}/organizations/controller`);
|
||||
const { FeatureAbilityFactory } = await import(`${importPath}/organizations/ability`);
|
||||
const { AppEnvironmentUtilService } = await import(`${importPath}/app-environments/util.service`);
|
||||
const { OrganizationsUtilService } = await import(`${importPath}/organizations/util.service`);
|
||||
|
||||
return {
|
||||
module: OrganizationsModule,
|
||||
imports: [await InstanceSettingsModule.register(configs)],
|
||||
imports: [await InstanceSettingsModule.register(configs), await AppEnvironmentsModule.register(configs)],
|
||||
controllers: [OrganizationsController],
|
||||
providers: [OrganizationsService, OrganizationRepository, FeatureAbilityFactory, AppEnvironmentUtilService],
|
||||
providers: [OrganizationsService, OrganizationRepository, FeatureAbilityFactory, OrganizationsUtilService],
|
||||
exports: [OrganizationsUtilService],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { catchDbException, isSuperAdmin } from '@helpers/utils.helper';
|
|||
import { ConfigScope, SSOType } from '@entities/sso_config.entity';
|
||||
import { WORKSPACE_STATUS, WORKSPACE_USER_STATUS } from '@modules/users/constants/lifecycle';
|
||||
import { CONSTRAINTS } from './constants';
|
||||
import { OrganizationInputs } from '@modules/setup-organization/types/organization-inputs';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationRepository extends Repository<Organization> {
|
||||
|
|
@ -106,7 +107,8 @@ export class OrganizationRepository extends Repository<Organization> {
|
|||
}, manager);
|
||||
}
|
||||
|
||||
createOne(name: string, slug: string, manager?: EntityManager): Promise<any> {
|
||||
createOne(organizationInputs: OrganizationInputs, manager?: EntityManager): Promise<any> {
|
||||
const { name, slug, isDefault } = organizationInputs;
|
||||
return dbTransactionWrap((manager: EntityManager) => {
|
||||
return catchDbException(() => {
|
||||
return manager.save(
|
||||
|
|
@ -120,6 +122,7 @@ export class OrganizationRepository extends Repository<Organization> {
|
|||
],
|
||||
name,
|
||||
slug,
|
||||
isDefault,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
|
@ -201,4 +204,27 @@ export class OrganizationRepository extends Repository<Organization> {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getDefaultWorkspaceOfInstance(): Promise<Organization>{
|
||||
return dbTransactionWrap(async (manager: EntityManager) => {
|
||||
try {
|
||||
return await manager.findOneOrFail(Organization, {
|
||||
where: { isDefault: true },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('No default workspace in this instance');
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async changeDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise<void> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
// First, unset any existing default workspace
|
||||
await manager.update(Organization, { isDefault: true }, { isDefault: false });
|
||||
|
||||
// Then set the new default workspace
|
||||
await manager.update(Organization, { id: organizationId }, { isDefault: true });
|
||||
}, manager || this.manager);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ConflictException, Injectable, NotAcceptableException } from '@nestjs/common';
|
||||
import { ConflictException, Injectable, NotAcceptableException, NotImplementedException } from '@nestjs/common';
|
||||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { isSuperAdmin } from 'src/helpers/utils.helper';
|
||||
import { dbTransactionWrap } from 'src/helpers/database.helper';
|
||||
|
|
@ -51,6 +51,11 @@ export class OrganizationsService implements IOrganizationsService {
|
|||
updatableData: OrganizationStatusUpdateDto
|
||||
): Promise<Organization> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const organization = await this.organizationRepository.findOne({ where: { id: organizationId } });
|
||||
if (organization.isDefault) {
|
||||
throw new NotAcceptableException('Default workspace cannot be archived');
|
||||
}
|
||||
|
||||
await this.organizationRepository.updateOne(organizationId, updatableData, manager);
|
||||
if (updatableData.status === WORKSPACE_STATUS.ACTIVE) {
|
||||
await this.licenseOrganizationService.validateOrganization(manager); //Check for only unarchiving
|
||||
|
|
@ -85,4 +90,8 @@ export class OrganizationsService implements IOrganizationsService {
|
|||
if (result) throw new ConflictException('Workspace name must be unique');
|
||||
return;
|
||||
}
|
||||
|
||||
async setDefaultWorkspace(organizationId: string, manager?: EntityManager): Promise<void> {
|
||||
throw new NotImplementedException('This feature is only available in Enterprise Edition');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ interface Features {
|
|||
[FEATURE_KEY.CREATE]: FeatureConfig;
|
||||
[FEATURE_KEY.CHECK_UNIQUE_ONBOARDING]: FeatureConfig;
|
||||
[FEATURE_KEY.WORKSPACE_STATUS_UPDATE]: FeatureConfig;
|
||||
[FEATURE_KEY.SET_DEFAULT]: FeatureConfig;
|
||||
}
|
||||
|
||||
export interface FeaturesConfig {
|
||||
|
|
|
|||
18
server/src/modules/organizations/util.service.ts
Normal file
18
server/src/modules/organizations/util.service.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { OrganizationRepository } from './repository';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { IOrganizationUtilService } from './interfaces/IUtilService';
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsUtilService implements IOrganizationUtilService {
|
||||
constructor(protected readonly organizationRepository: OrganizationRepository) {}
|
||||
|
||||
async validateWorkspaceExists(workspaceId: string) {
|
||||
const existingWorkspace = await this.organizationRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
});
|
||||
if (!existingWorkspace) {
|
||||
throw new BadRequestException(`Invalid workspaceId: ${workspaceId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { EditUserRoleDto } from './dto';
|
||||
import { RolesUtilService } from './util.service';
|
||||
import { ERROR_HANDLER } from '../group-permissions/constants/error';
|
||||
import { _ } from 'lodash';
|
||||
import { LicenseUserService } from '@modules/licensing/services/user.service';
|
||||
import { dbTransactionWrap } from '@helpers/database.helper';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { RolesRepository } from './repository';
|
||||
import { IRolesService } from './interfaces/IService';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { dbTransactionWrap } from '@helpers/database.helper';
|
||||
import { LicenseUserService } from '@modules/licensing/services/user.service';
|
||||
import { ERROR_HANDLER } from '@modules/group-permissions/constants/error';
|
||||
import { _ } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class RolesService implements IRolesService {
|
||||
|
|
|
|||
|
|
@ -368,8 +368,8 @@ export class SessionUtilService {
|
|||
async #onboardingFlags(user: User) {
|
||||
let isFirstUserOnboardingCompleted = true;
|
||||
let isOnboardingCompleted = true;
|
||||
const isOnboardingQuestionsEnabled =
|
||||
this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true';
|
||||
// const isOnboardingQuestionsEnabled =
|
||||
// this.configService.get<string>('ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS') === 'true';
|
||||
|
||||
const instanceUsersCount = await this.userRepository.count({
|
||||
where: { status: USER_STATUS.ACTIVE },
|
||||
|
|
@ -383,14 +383,14 @@ export class SessionUtilService {
|
|||
}
|
||||
|
||||
/* Signed up user check */
|
||||
if (
|
||||
instanceUsersCount > 1 &&
|
||||
isOnboardingQuestionsEnabled &&
|
||||
user.onboardingStatus !== OnboardingStatus.ONBOARDING_COMPLETED
|
||||
) {
|
||||
/* Signed up user went through onboarding flow, didn't complete */
|
||||
isOnboardingCompleted = false;
|
||||
}
|
||||
// if (
|
||||
// instanceUsersCount > 1 &&
|
||||
// isOnboardingQuestionsEnabled &&
|
||||
// user.onboardingStatus !== OnboardingStatus.ONBOARDING_COMPLETED
|
||||
// ) {
|
||||
// /* Signed up user went through onboarding flow, didn't complete */
|
||||
// isOnboardingCompleted = false;
|
||||
// }
|
||||
|
||||
return { isFirstUserOnboardingCompleted, isOnboardingCompleted };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ export class SetupOrganizationsController implements ISetupOrganizationsControll
|
|||
@Res({ passthrough: true }) response: Response
|
||||
) {
|
||||
const result = await this.setupOrganizationsService.create(
|
||||
organizationCreateDto.name,
|
||||
organizationCreateDto.slug,
|
||||
{ name: organizationCreateDto.name, slug: organizationCreateDto.slug },
|
||||
user
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { User } from 'src/entities/user.entity';
|
||||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { OrganizationInputs } from '../types/organization-inputs';
|
||||
|
||||
export interface ISetupOrganizationsService {
|
||||
create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization>;
|
||||
create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { User } from 'src/entities/user.entity';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { Organization } from '@entities/organization.entity';
|
||||
import { OrganizationInputs } from '../types/organization-inputs';
|
||||
|
||||
export interface ISetupOrganizationsUtilService {
|
||||
create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization>;
|
||||
create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import { User } from 'src/entities/user.entity';
|
|||
import { EntityManager } from 'typeorm';
|
||||
import { SetupOrganizationsUtilService } from './util.service';
|
||||
import { ISetupOrganizationsService } from './interfaces/IService';
|
||||
import { OrganizationInputs } from './types/organization-inputs';
|
||||
|
||||
@Injectable()
|
||||
export class SetupOrganizationsService implements ISetupOrganizationsService {
|
||||
constructor(protected readonly setupOrganizationsUtilService: SetupOrganizationsUtilService) {}
|
||||
|
||||
async create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization> {
|
||||
return this.setupOrganizationsUtilService.create(name, slug, user, manager);
|
||||
async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization> {
|
||||
return this.setupOrganizationsUtilService.create(organizationInputs, user, manager);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
export interface OrganizationInputs {
|
||||
name: string;
|
||||
slug: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import { OrganizationUsersRepository } from '@modules/organization-users/reposit
|
|||
import { SampleDataSourceService } from '@modules/data-sources/services/sample-ds.service';
|
||||
import { ISetupOrganizationsUtilService } from './interfaces/IUtilService';
|
||||
import { TooljetDbTableOperationsService } from '@modules/tooljet-db/services/tooljet-db-table-operations.service';
|
||||
import { OrganizationInputs } from './types/organization-inputs';
|
||||
|
||||
@Injectable()
|
||||
export class SetupOrganizationsUtilService implements ISetupOrganizationsUtilService {
|
||||
|
|
@ -31,9 +32,9 @@ export class SetupOrganizationsUtilService implements ISetupOrganizationsUtilSer
|
|||
protected readonly organizationUserRepository: OrganizationUsersRepository
|
||||
) {}
|
||||
|
||||
async create(name: string, slug: string, user?: User, manager?: EntityManager): Promise<Organization> {
|
||||
async create(organizationInputs: OrganizationInputs, user?: User, manager?: EntityManager): Promise<Organization> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const organization = await this.organizationRepository.createOne(name, slug, manager);
|
||||
const organization = await this.organizationRepository.createOne(organizationInputs, manager);
|
||||
await this.appEnvironmentUtilService.createDefaultEnvironments(organization.id, manager);
|
||||
await this.groupPermissionUtilService.createDefaultGroups(organization.id, manager);
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export class UsersModule {
|
|||
imports: [await SessionModule.register(configs)],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService, UserRepository, UsersUtilService, FeatureAbilityFactory],
|
||||
exports: [UsersUtilService],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { Organization } from '@entities/organization.entity';
|
|||
import { OrganizationUser } from '@entities/organization_user.entity';
|
||||
import { isSuperAdmin } from '@helpers/utils.helper';
|
||||
import * as uuid from 'uuid';
|
||||
import { USER_ROLE } from '@modules/group-permissions/constants';
|
||||
|
||||
type UserFilterOptions = { searchText?: string; status?: string; page?: number };
|
||||
|
||||
|
|
@ -168,6 +169,18 @@ export class UserRepository extends Repository<User> {
|
|||
await manager.upsert(UserDetails, updatableParams, conflictsPaths);
|
||||
}
|
||||
|
||||
async getUserWithAdminRole(organizationId: string, manager?: EntityManager): Promise<User | null> {
|
||||
return dbTransactionWrap((manager: EntityManager) => {
|
||||
return manager
|
||||
.createQueryBuilder(User, 'user')
|
||||
.innerJoin('user.userGroups', 'groupUsers')
|
||||
.innerJoin('groupUsers.group', 'group')
|
||||
.where('group.name = :groupName', { groupName: USER_ROLE.ADMIN })
|
||||
.andWhere('group.organizationId = :organizationId', { organizationId })
|
||||
.getOne();
|
||||
}, manager || this.manager);
|
||||
}
|
||||
|
||||
async findByEmail(
|
||||
email: string,
|
||||
organizationId?: string,
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
|
|||
|
||||
export interface IVersionUtilService {
|
||||
updateVersion(appVersion: AppVersion, appVersionUpdateDto: AppVersionUpdateDto): Promise<void>;
|
||||
fetchVersions(appId: string): Promise<AppVersion[]>;
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue