diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/steps.js b/frontend/src/AppBuilder/WidgetManager/widgets/steps.js
index 0a6a2cd575..9e71a89f90 100644
--- a/frontend/src/AppBuilder/WidgetManager/widgets/steps.js
+++ b/frontend/src/AppBuilder/WidgetManager/widgets/steps.js
@@ -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' },
},
},
};
diff --git a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js
index 252e387d70..79acc2c461 100644
--- a/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/dataQuerySlice.js
@@ -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],
];
});
diff --git a/frontend/src/Editor/ActionTypes.js b/frontend/src/Editor/ActionTypes.js
index 0bad71b3ac..e3fb69f95d 100644
--- a/frontend/src/Editor/ActionTypes.js
+++ b/frontend/src/Editor/ActionTypes.js
@@ -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',
},
];
diff --git a/frontend/src/Editor/Components/DraftEditor.jsx b/frontend/src/Editor/Components/DraftEditor.jsx
index b2c8b61994..6734f3c865 100644
--- a/frontend/src/Editor/Components/DraftEditor.jsx
+++ b/frontend/src/Editor/Components/DraftEditor.jsx
@@ -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) {
diff --git a/frontend/src/Editor/Components/Steps.jsx b/frontend/src/Editor/Components/Steps.jsx
index 4a42e9362e..4662c92bc6 100644
--- a/frontend/src/Editor/Components/Steps.jsx
+++ b/frontend/src/Editor/Components/Steps.jsx
@@ -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 && (
-
- {steps?.map((item) => (
-
stepsSelectable && activeStepHandler(item.id)}
- style={dynamicStyle}
- >
- {theme == 'titles' && item.name}
-
- ))}
+
+
+ {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 (
+
{/* using index as key to avoid issues due to duplicate step ids */}
+
+ stepsSelectable && handleStepClick(step.id)}
+ className={`milestone ${theme === 'numbers' ? 'numbers' : ''} ${isDisabled || isStepDisabled ? 'disabled' : ''
+ } ${isCompleted ? 'completed' : isActive ? 'active' : 'incomplete'}`}
+ >
+ {theme === 'numbers' ? (
+ index + 1
+ ) : (
+ <>
+
+ {theme === 'titles' && (
+
+ {step.name}
+
+ )}
+ >
+ )}
+
+
+
+ {index < filteredSteps.length - 1 && (
+
+ )}
+
+ );
+ })}
- )
+
);
};
diff --git a/frontend/src/Editor/Components/Steps.scss b/frontend/src/Editor/Components/Steps.scss
new file mode 100644
index 0000000000..c61f185a73
--- /dev/null
+++ b/frontend/src/Editor/Components/Steps.scss
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/Editor/WidgetManager/configs/index.js b/frontend/src/Editor/WidgetManager/configs/index.js
index 93e45fd06c..b26b6c84a2 100644
--- a/frontend/src/Editor/WidgetManager/configs/index.js
+++ b/frontend/src/Editor/WidgetManager/configs/index.js
@@ -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,
diff --git a/frontend/src/Editor/WidgetManager/configs/steps.js b/frontend/src/Editor/WidgetManager/configs/steps.js
deleted file mode 100644
index a39c634919..0000000000
--- a/frontend/src/Editor/WidgetManager/configs/steps.js
+++ /dev/null
@@ -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: '' },
- },
- },
-};
diff --git a/frontend/src/_styles/tabler.scss b/frontend/src/_styles/tabler.scss
index 086712dfa5..6b021cce0d 100644
--- a/frontend/src/_styles/tabler.scss
+++ b/frontend/src/_styles/tabler.scss
@@ -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
}
-}
+}
\ No newline at end of file
diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss
index b77c83ba30..86019a158a 100644
--- a/frontend/src/_styles/theme.scss
+++ b/frontend/src/_styles/theme.scss
@@ -4760,15 +4760,18 @@ input[type="text"] {
.folder-list {
overflow-y: scroll;
scrollbar-width: thin;
- scrollbar-color: #888 transparent;
+ scrollbar-color: #888 transparent;
+
&:hover {
&::-webkit-scrollbar {
display: block;
width: 5px;
}
+
&::-webkit-scrollbar-thumb {
background-color: #888;
}
+
&::-webkit-scrollbar-track {
background-color: transparent;
}
@@ -6518,6 +6521,7 @@ div#driver-page-overlay {
// steps-widget
a.step-item-disabled {
text-decoration: none;
+ opacity: 0.5;
}
.steps {
@@ -6527,34 +6531,45 @@ a.step-item-disabled {
.step-item.active~.step-item:after,
.step-item.active~.step-item:before {
- background: #f3f5f5 !important;
+ background: var(--incompletedAccent) !important;
}
.step-item.active:before {
- background: #ffffff !important;
+ background: transparent !important;
}
.steps .step-item.active:before {
- border-color: #b4b2b2 !important;
+ border-color: var(--completedAccent) !important;
}
.steps-item {
color: var(--textColor) !important;
}
+
+.step-item {
+ &.completed-label {
+ color: var(--completedLabel) !important;
+ }
+
+ &.incompleted-label {
+ color: var(--incompletedLabel) !important;
+ }
+
+ &.active-label {
+ color: var(--currentStepLabel) !important;
+ }
+}
+
.step-item:before {
- background: var(--bgColor) !important;
+ background-color: var(--completedAccent) !important;
// remaining code
}
.step-item:after {
- background: var(--bgColor) !important;
+ background: var(--completedAccent) !important;
}
-.step-item.active~.step-item {
- color: var(--textColor) !important;
- ;
-}
.notification-center-badge {
@@ -9872,25 +9887,30 @@ tbody {
.workspace-settings-table-wrap {
max-width: 880px;
margin: 0 auto;
- .tj-user-table-wrapper{
+
+ .tj-user-table-wrapper {
padding-right: 4px;
- }
- &:hover{
- .tj-user-table-wrapper{
- padding-right: 0px;
- }
- ::-webkit-scrollbar{
- display: block;
- width: 4px;
- }
- ::-webkit-scrollbar-track{
- background: var(--base);
- }
- ::-webkit-scrollbar-thumb{
- background: var(--slate7);
- border-radius: 6px;
- }
- }
+ }
+
+ &:hover {
+ .tj-user-table-wrapper {
+ padding-right: 0px;
+ }
+
+ ::-webkit-scrollbar {
+ display: block;
+ width: 4px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background: var(--base);
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: var(--slate7);
+ border-radius: 6px;
+ }
+ }
}
@@ -12056,8 +12076,10 @@ tbody {
letter-spacing: -0.02em;
}
}
+
.sidebar-list-wrap.sidebar-list-wrap-with-banner.isAdmin {
height: calc(100vh - 371px);
+
&.resource-limit-reached {
height: calc(100vh - 371px);
}
@@ -15801,6 +15823,7 @@ textarea.tj-text-input-widget{
.rest-api-options-codehinter {
height: 100%;
+
.cm-content>.cm-line {
// max-width: 357px !important;
}
@@ -16232,19 +16255,20 @@ fieldset:disabled {
}
.datepicker-validation-half {
- flex:1 1 calc(50% - 8px);
+ flex: 1 1 calc(50% - 8px);
}
.date-validation-wrapper {
.field {
- height:24px;
+ height: 24px;
}
.code-flex-wrapper {
- flex-wrap:wrap;
+ flex-wrap: wrap;
}
+
margin-bottom: 3px;
}
@@ -16253,57 +16277,60 @@ fieldset:disabled {
}
- .react-datepicker__day--disabled {
+.react-datepicker__day--disabled {
+ color: #ccc !important;
+}
+
+.react-datepicker__time-list {
+ li.react-datepicker__time-list-item--disabled.react-datepicker__time-list-item {
color: #ccc !important;
}
-
- .react-datepicker__time-list{
- li.react-datepicker__time-list-item--disabled.react-datepicker__time-list-item {
- color: #ccc !important;
- }
- }
-
- .inspector-validation-date-picker {
- .react-datepicker-wrapper{
- input {
- background-color: #fff;
- }
- input.dark-theme {
- background-color: var(--slate3);
- color: var(--slate12);
- }
+}
+.inspector-validation-date-picker {
+ .react-datepicker-wrapper {
+ input {
+ background-color: #fff;
}
-
+
+ input.dark-theme {
+ background-color: var(--slate3);
+ color: var(--slate12);
+ }
+
}
+}
-.datetimepicker-component, #component-portal, .custom-inspector-validation-time-picker {
+
+.datetimepicker-component,
+#component-portal,
+.custom-inspector-validation-time-picker {
.datepicker-component {
.react-datepicker {
border-radius: 10px;
box-shadow: 8px 8px 16px 0px #3032331A;
- height:auto;
+ height: auto;
}
}
-
+
.react-datepicker-time-component {
border-radius: 10px;
- width:auto;
+ width: auto;
- .custom-time-input{
- border-left:none;
- border-radius:10px;
+ .custom-time-input {
+ border-left: none;
+ border-radius: 10px;
box-shadow: 8px 8px 16px 0px #3032331A;
}
.time-input-body {
- padding-bottom:0px;
+ padding-bottom: 0px;
}
-
+
.time-col {
height: 200px;
}
@@ -16312,28 +16339,32 @@ fieldset:disabled {
border-radius: 10px;
box-shadow: 8px 8px 16px 0px #3032331A;
}
-
- .react-datepicker-time__input-container{
- border-radius:10px;
+
+ .react-datepicker-time__input-container {
+ border-radius: 10px;
}
}
-
+
.dark-theme {
- .react-datepicker__year-text, .react-datepicker__month-text {
+
+ .react-datepicker__year-text,
+ .react-datepicker__month-text {
color: #fff;
}
- .react-datepicker__year-text:hover, .react-datepicker__month-text:hover {
- background-color: #9ba1a6 ;
+ .react-datepicker__year-text:hover,
+ .react-datepicker__month-text:hover {
+ background-color: #9ba1a6;
}
}
- .tj-datepicker-widget-year-selector:hover, .tj-datepicker-widget-month-selector:hover {
- padding:1px 6px;
+ .tj-datepicker-widget-year-selector:hover,
+ .tj-datepicker-widget-month-selector:hover {
+ padding: 1px 6px;
}
- .react-datepicker{
+ .react-datepicker {
display: grid;
grid-auto-flow: column;
border-top-right-radius: 0rem;
@@ -16346,48 +16377,49 @@ fieldset:disabled {
justify-content: center;
align-items: center;
}
+
.react-datepicker__year-wrapper {
- display:grid;
- grid-template-columns:repeat(3, 1fr);
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
max-width: unset;
- gap:10px;
+ gap: 10px;
}
.react-datepicker {
border-radius: 10px;
}
- .react-datepicker__header--custom{
+ .react-datepicker__header--custom {
height: 34px;
margin-bottom: 14px;
}
- .react-datepicker__year--container{
- height:208px;
+ .react-datepicker__year--container {
+ height: 208px;
width: 250px;
box-shadow: 8px 8px 16px 0px #3032331A;
border-radius: 10px;
}
.react-datepicker__year-text--selected {
- background-color: #4368E3 !important;
- height:24px;
- width:61.33px;
- border-radius: 8px;
- color: #fff ;
+ background-color: #4368E3 !important;
+ height: 24px;
+ width: 61.33px;
+ border-radius: 8px;
+ color: #fff;
}
- .react-datepicker__year-text{
- font-family:'IBM Plex Sans' ;
+ .react-datepicker__year-text {
+ font-family: 'IBM Plex Sans';
font-size: 12px;
line-height: 16px;
text-align: center;
font-weight: 400;
- height:24px;
- width:61.33px;
+ height: 24px;
+ width: 61.33px;
justify-content: center;
align-items: center;
- display:flex;
+ display: flex;
}
}
@@ -16402,42 +16434,42 @@ fieldset:disabled {
}
.react-datepicker__month-container {
- height:208px;
+ height: 208px;
width: 250px;
box-shadow: 8px 8px 16px 0px #3032331A;
border-radius: 10px;
}
.react-datepicker__monthPicker {
- display:flex;
+ display: flex;
flex-direction: column;
- gap:10px;
+ gap: 10px;
}
.react-datepicker__month-text--selected {
background-color: #4368E3 !important;
- height:24px;
- width:61.33px;
+ height: 24px;
+ width: 61.33px;
border-radius: 8px;
- color: #fff ;
+ color: #fff;
}
.react-datepicker__month-wrapper {
- display:flex;
- gap:24px;
+ display: flex;
+ gap: 24px;
}
.react-datepicker__month-text {
- font-family:'IBM Plex Sans' ;
+ font-family: 'IBM Plex Sans';
font-size: 12px;
line-height: 16px;
text-align: center;
font-weight: 400;
- height:24px;
- width:61.33px;
+ height: 24px;
+ width: 61.33px;
justify-content: center;
align-items: center;
- display:flex;
+ display: flex;
}
}
@@ -16447,7 +16479,7 @@ fieldset:disabled {
.react-datepicker__month-container {
width: 100%;
- width:250px;
+ width: 250px;
}
.react-datepicker__input-time-container {
@@ -16462,12 +16494,12 @@ fieldset:disabled {
color: #ccc !important;
pointer-events: none;
}
-
+
.react-datepicker-time__input {
margin-left: 0px !important;
.dark-time-input {
- color:#f4f6fa !important;
+ color: #f4f6fa !important;
background-color: var(--surfaces-surface-01) !important;
}
}
@@ -16475,15 +16507,15 @@ fieldset:disabled {
.react-datepicker-wrapper {
width: 100%;
}
-
+
.react-datepicker-time__caption {
- display:none;
+ display: none;
}
.custom-time-input {
background-color: #fff;
border-left: 1px solid #CCD1D5;
- border-top-right-radius: 10px;
+ border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
@@ -16497,18 +16529,18 @@ fieldset:disabled {
border-bottom: 1px solid #CCD1D5;
font-weight: 500;
font-family: 'IBM Plex Sans';
- color:#ACB2B9;
+ color: #ACB2B9;
}
-
+
.time-input-body {
padding-bottom: 12px;
}
.time-col {
margin-top: 5px;
- overflow-y: auto;
+ overflow-y: auto;
overflow-x: hidden;
- scrollbar-width: none;
+ scrollbar-width: none;
height: 265px;
width: 62px;
}
@@ -16516,12 +16548,12 @@ fieldset:disabled {
.selected-time {
background-color: #4368E3 !important;
border-radius: 6px;
- color:#fff;
+ color: #fff;
}
.time-item {
- width: 50px;
- height:22px;
+ width: 50px;
+ height: 22px;
display: flex;
justify-content: center;
align-items: center;
@@ -16861,16 +16893,17 @@ section.ai-message-prompt-input-wrapper {
.tj-inspector-timepicker.dark-theme {
- .react-datepicker {
- color:#f4f6fa !important;
+ .react-datepicker {
+ color: #f4f6fa !important;
background-color: var(--surfaces-surface-01) !important;
}
- .react-datepicker, .react-datepicker__header {
+ .react-datepicker,
+ .react-datepicker__header {
border: 1px solid var(--borders-default);
background-color: #1f2936;
- .react-datepicker-time__header{
+ .react-datepicker-time__header {
color: #fff !important;
}
@@ -16878,25 +16911,27 @@ section.ai-message-prompt-input-wrapper {
}
.tj-inspector-timepicker {
- padding:0px !important;
+ padding: 0px !important;
.react-datepicker__time-list {
- scrollbar-width: none;
+ scrollbar-width: none;
}
.react-datepicker__triangle {
- display:none;
+ display: none;
}
}
-.custom-inspector-validation-date-picker, .custom-inspector-validation-time-picker {
+.custom-inspector-validation-date-picker,
+.custom-inspector-validation-time-picker {
flex-basis: 100% !important;
font-family: monospace;
font-size: 12px;
- height:32px;
-
+ height: 32px;
+
.react-datepicker-wrapper {
width: 100%;
+
input {
width: 100%;
border: 1px solid var(--slate7);
@@ -16904,23 +16939,23 @@ section.ai-message-prompt-input-wrapper {
background-color: var(--base);
background-color: #fff;
color: rgb(0, 92, 197);
- height:32px;
+ height: 32px;
}
input.dark-theme {
background-color: #272822;
color: rgb(174, 129, 255);
-
+
}
}
-
+
}
.custom-inspector-validation-time-picker {
.custom-time-input {
- border-left:none;
- border-radius:10px;
+ border-left: none;
+ border-radius: 10px;
}
.time-col {
@@ -16928,19 +16963,21 @@ section.ai-message-prompt-input-wrapper {
}
.react-datepicker__input-time-container {
- border-radius:10px;
+ border-radius: 10px;
}
-
-
+
+
}
.custom-inspector-validation-time-picker-popper {
- border-radius:10px;
+ border-radius: 10px;
}
-.input-date-display-format, .input-date-time-format {
+.input-date-display-format,
+.input-date-time-format {
height: 60px;
+
.hide-fx {
opacity: 0;
transition: opacity 0.3s ease;
@@ -16959,8 +16996,9 @@ section.ai-message-prompt-input-wrapper {
color: white;
}
- .react-datepicker__day:hover, .react-datepicker__day--selecting-range-end {
- background-color: var(--interactive-overlays-fill-hover) !important ;
+ .react-datepicker__day:hover,
+ .react-datepicker__day--selecting-range-end {
+ background-color: var(--interactive-overlays-fill-hover) !important;
}
.react-datepicker__day--keyboard-selected {
@@ -16983,15 +17021,17 @@ section.ai-message-prompt-input-wrapper {
.tj-daterange-widget {
- border-radius:10px;
+ border-radius: 10px;
box-shadow: 0px 8px 16px 0px #3032331A !important;
font-family: 'IBM Plex Sans';
- .react-datepicker__day--in-selecting-range, .react-datepicker__day--in-range {
- border-radius:0px;
+ .react-datepicker__day--in-selecting-range,
+ .react-datepicker__day--in-range {
+ border-radius: 0px;
background-color: #4368E31A !important;
}
- .react-datepicker__header{
+
+ .react-datepicker__header {
background-color: var(--surfaces-surface-01);
padding: 6px 0px;
border: none;
@@ -17002,44 +17042,48 @@ section.ai-message-prompt-input-wrapper {
background-color: #ededee !important;
}
- .react-datepicker__day--selecting-range-start, .react-datepicker__day--selected, .react-datepicker__day--range-end {
- border-radius:8px !important;
+ .react-datepicker__day--selecting-range-start,
+ .react-datepicker__day--selected,
+ .react-datepicker__day--range-end {
+ border-radius: 8px !important;
background-color: #4368E3 !important;
color: #fff !important;
}
- .react-datepicker__day--in-range:has(+ .react-datepicker__day--range-end), .react-datepicker__day--in-selecting-range:has(+ .react-datepicker__day--selecting-range-end) {
+ .react-datepicker__day--in-range:has(+ .react-datepicker__day--range-end),
+ .react-datepicker__day--in-selecting-range:has(+ .react-datepicker__day--selecting-range-end) {
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
-
+
.react-datepicker__day--in-range:has(+ .react-datepicker__day--range-end) {
box-shadow: 10px 0 0 0px #4368E31A;
}
- .react-datepicker__day--range-start + .react-datepicker__day--in-range, .react-datepicker__day--selecting-range-start + .react-datepicker__day--in-selecting-range{
+ .react-datepicker__day--range-start+.react-datepicker__day--in-range,
+ .react-datepicker__day--selecting-range-start+.react-datepicker__day--in-selecting-range {
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
- .react-datepicker__day--range-start + .react-datepicker__day--in-range {
+ .react-datepicker__day--range-start+.react-datepicker__day--in-range {
box-shadow: -10px 0 0 0px #4368E31A;
}
-
+
.react-datepicker__week {
- .react-datepicker__day--in-range:first-of-type,
- .react-datepicker__day--in-selecting-range:first-of-type,
- .react-datepicker__day--outside-month + .react-datepicker__day--in-range,
- .react-datepicker__day--outside-month + .react-datepicker__day--in-selecting-range{
+ .react-datepicker__day--in-range:first-of-type,
+ .react-datepicker__day--in-selecting-range:first-of-type,
+ .react-datepicker__day--outside-month+.react-datepicker__day--in-range,
+ .react-datepicker__day--outside-month+.react-datepicker__day--in-selecting-range {
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
- .react-datepicker__day--in-range:last-of-type,
- .react-datepicker__day--in-selecting-range:last-of-type,
- .react-datepicker__day--in-range:has(+ .react-datepicker__day--outside-month),
- .react-datepicker__day--in-selecting-range:has(+ .react-datepicker__day--outside-month){
+ .react-datepicker__day--in-range:last-of-type,
+ .react-datepicker__day--in-selecting-range:last-of-type,
+ .react-datepicker__day--in-range:has(+ .react-datepicker__day--outside-month),
+ .react-datepicker__day--in-selecting-range:has(+ .react-datepicker__day--outside-month) {
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
@@ -17057,8 +17101,8 @@ section.ai-message-prompt-input-wrapper {
}
.tj-datepicker-widget-right {
- position: absolute;
- right: 10px;
+ position: absolute;
+ right: 10px;
}
.tj-datepicker-widget-left {
@@ -17081,41 +17125,42 @@ section.ai-message-prompt-input-wrapper {
}
.react-datepicker {
- border-radius:10px !important;
- border:none;
+ border-radius: 10px !important;
+ border: none;
}
-
+
}
-.tj-daterangepicker-widget-month-selector, .tj-daterangepicker-widget-year-selector {
- appearance: none;
- -moz-appearance: none;
- -webkit-appearance: none;
- padding-right: 4px;
- /* Add some padding on the right to create space for custom arrow */
- background-image: url('data:image/svg+xml;utf8,
');
- /* Add a custom arrow (you can use your own SVG) */
- background-repeat: no-repeat;
- background-position: right center;
- border: none;
- /* Remove the default border */
- padding: 8px;
- /* Adjust padding as needed */
- cursor: pointer;
- /* Add pointer cursor for better usability */
- background: none;
- padding: 0px;
- height: 24px;
- text-align: center;
- color: var(--text-primary);
- font-weight: 500;
- width:auto;
+.tj-daterangepicker-widget-month-selector,
+.tj-daterangepicker-widget-year-selector {
+ appearance: none;
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ padding-right: 4px;
+ /* Add some padding on the right to create space for custom arrow */
+ background-image: url('data:image/svg+xml;utf8,
');
+ /* Add a custom arrow (you can use your own SVG) */
+ background-repeat: no-repeat;
+ background-position: right center;
+ border: none;
+ /* Remove the default border */
+ padding: 8px;
+ /* Adjust padding as needed */
+ cursor: pointer;
+ /* Add pointer cursor for better usability */
+ background: none;
+ padding: 0px;
+ height: 24px;
+ text-align: center;
+ color: var(--text-primary);
+ font-weight: 500;
+ width: auto;
}
.datepicker-widget {
- .react-datepicker-wrapper{
- width:100% !important;
+ .react-datepicker-wrapper {
+ width: 100% !important;
}
}
@@ -17129,26 +17174,29 @@ section.ai-message-prompt-input-wrapper {
}
.tj-daterange-widget.react-datepicker-month-component {
- border-radius:10px;
+ border-radius: 10px;
box-shadow: 0px 8px 16px 0px #3032331A !important;
font-family: 'IBM Plex Sans';
+
.react-datepicker__month-container {
box-shadow: none !important;
}
-
+
.react-datepicker__month-text {
- height:26px !important;
+ height: 26px !important;
margin: 0px;
- width:100% !important;
+ width: 100% !important;
}
- .react-datepicker__month-text--in-selecting-range, .react-datepicker__month-text--in-range {
- border-radius:0px;
+ .react-datepicker__month-text--in-selecting-range,
+ .react-datepicker__month-text--in-range {
+ border-radius: 0px;
background-color: #4368E31A !important;
- color:#000;
+ color: #000;
}
- .react-datepicker__header{
+
+ .react-datepicker__header {
background-color: var(--surfaces-surface-01);
padding: 6px 0px;
border: none;
@@ -17161,45 +17209,49 @@ section.ai-message-prompt-input-wrapper {
}
- .react-datepicker__month-text--selecting-range-start, .react-datepicker__month-text--selected, .react-datepicker__month-text--range-end {
- border-radius:8px !important;
+ .react-datepicker__month-text--selecting-range-start,
+ .react-datepicker__month-text--selected,
+ .react-datepicker__month-text--range-end {
+ border-radius: 8px !important;
background-color: #4368E3 !important;
color: #fff !important;
}
- .react-datepicker__month-text--in-range:has(+ .react-datepicker__month-text--range-end), .react-datepicker__month-text--in-selecting-range:has(+ .react-datepicker__month-text--selecting-range-end) {
+ .react-datepicker__month-text--in-range:has(+ .react-datepicker__month-text--range-end),
+ .react-datepicker__month-text--in-selecting-range:has(+ .react-datepicker__month-text--selecting-range-end) {
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
-
+
.react-datepicker__month-text--in-range:has(+ .react-datepicker__month-text--range-end) {
box-shadow: 10px 0 0 0px #4368E31A;
}
- .react-datepicker__month-text--range-start + .react-datepicker__month-text--in-range, .react-datepicker__month-text--selecting-range-start + .react-datepicker__month-text--in-selecting-range{
+ .react-datepicker__month-text--range-start+.react-datepicker__month-text--in-range,
+ .react-datepicker__month-text--selecting-range-start+.react-datepicker__month-text--in-selecting-range {
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
- .react-datepicker__month-text--range-start + .react-datepicker__month-text--in-range {
+ .react-datepicker__month-text--range-start+.react-datepicker__month-text--in-range {
box-shadow: -10px 0 0 0px #4368E31A;
}
-
- .react-datepicker__month-wrapper{
- gap:0px !important;
- .react-datepicker__month-text--in-range:first-of-type,
- .react-datepicker__month-text--in-selecting-range:first-of-type,
- .react-datepicker__month-text--outside-month-text + .react-datepicker__month-text--in-range,
- .react-datepicker__month-text--outside-month-text + .react-datepicker__month-text--in-selecting-range{
+ .react-datepicker__month-wrapper {
+ gap: 0px !important;
+
+ .react-datepicker__month-text--in-range:first-of-type,
+ .react-datepicker__month-text--in-selecting-range:first-of-type,
+ .react-datepicker__month-text--outside-month-text+.react-datepicker__month-text--in-range,
+ .react-datepicker__month-text--outside-month-text+.react-datepicker__month-text--in-selecting-range {
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
- .react-datepicker__month-text--in-range:last-of-type,
- .react-datepicker__month-text--in-selecting-range:last-of-type,
- .react-datepicker__month-text--in-range:has(+ .react-datepicker__month-text--outside-month-text),
- .react-datepicker__month-text--in-selecting-range:has(+ .react-datepicker__month-text--outside-month-text){
+ .react-datepicker__month-text--in-range:last-of-type,
+ .react-datepicker__month-text--in-selecting-range:last-of-type,
+ .react-datepicker__month-text--in-range:has(+ .react-datepicker__month-text--outside-month-text),
+ .react-datepicker__month-text--in-selecting-range:has(+ .react-datepicker__month-text--outside-month-text) {
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
@@ -17219,44 +17271,47 @@ section.ai-message-prompt-input-wrapper {
}
.tj-daterange-widget.react-datepicker-year-component {
- border-radius:10px;
+ border-radius: 10px;
box-shadow: 0px 8px 16px 0px #3032331A !important;
font-family: 'IBM Plex Sans';
+
.react-datepicker__year-container {
box-shadow: none !important;
}
- .react-datepicker__year-wrapper{
- gap:0px !important;
+ .react-datepicker__year-wrapper {
+ gap: 0px !important;
- .react-datepicker__year-text--in-range:first-of-type,
- .react-datepicker__year-text--in-selecting-range:first-of-type{
+ .react-datepicker__year-text--in-range:first-of-type,
+ .react-datepicker__year-text--in-selecting-range:first-of-type {
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
- .react-datepicker__year-text--in-range:last-of-type,
- .react-datepicker__year-text--in-selecting-range:last-of-type{
+ .react-datepicker__year-text--in-range:last-of-type,
+ .react-datepicker__year-text--in-selecting-range:last-of-type {
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
}
-
+
.react-datepicker__year-text {
- height:26px !important;
+ height: 26px !important;
margin-top: 5px !important;
- margin-bottom:5px !important;
+ margin-bottom: 5px !important;
margin: 0px;
- width:62px !important;
+ width: 62px !important;
}
- .react-datepicker__year-text--in-selecting-range, .react-datepicker__year-text--in-range {
- border-radius:0px;
+ .react-datepicker__year-text--in-selecting-range,
+ .react-datepicker__year-text--in-range {
+ border-radius: 0px;
background-color: #4368E31A !important;
- color:#000;
+ color: #000;
}
- .react-datepicker__header{
+
+ .react-datepicker__header {
background-color: var(--surfaces-surface-01);
padding: 6px 0px;
border: none;
@@ -17269,31 +17324,35 @@ section.ai-message-prompt-input-wrapper {
}
- .react-datepicker__year-text--selecting-range-start, .react-datepicker__year-text--selected, .react-datepicker__year-text--range-end {
- border-radius:8px !important;
+ .react-datepicker__year-text--selecting-range-start,
+ .react-datepicker__year-text--selected,
+ .react-datepicker__year-text--range-end {
+ border-radius: 8px !important;
background-color: #4368E3 !important;
color: #fff !important;
}
- .react-datepicker__year-text--in-range:has(+ .react-datepicker__year-text--range-end), .react-datepicker__year-text--in-selecting-range:has(+ .react-datepicker__year-text--selecting-range-end) {
+ .react-datepicker__year-text--in-range:has(+ .react-datepicker__year-text--range-end),
+ .react-datepicker__year-text--in-selecting-range:has(+ .react-datepicker__year-text--selecting-range-end) {
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
-
+
.react-datepicker__year-text--in-range:has(+ .react-datepicker__year-text--range-end) {
box-shadow: 10px 0 0 0px #4368E31A;
}
- .react-datepicker__year-text--range-start + .react-datepicker__year-text--in-range, .react-datepicker__year-text--selecting-range-start + .react-datepicker__year-text--in-selecting-range{
+ .react-datepicker__year-text--range-start+.react-datepicker__year-text--in-range,
+ .react-datepicker__year-text--selecting-range-start+.react-datepicker__year-text--in-selecting-range {
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
- .react-datepicker__year-text--range-start + .react-datepicker__year-text--in-range {
+ .react-datepicker__year-text--range-start+.react-datepicker__year-text--in-range {
box-shadow: -10px 0 0 0px #4368E31A;
}
-
-
+
+
}
.dark-theme {
@@ -18783,6 +18842,7 @@ section.ai-message-prompt-input-wrapper {
font-style: normal;
font-weight: 400;
line-height: 18px;
+
&.dark {
background: #FFFAEB !important;
}
diff --git a/server/data-migrations/1742369436314-StepsV2Migration.ts b/server/data-migrations/1742369436314-StepsV2Migration.ts
new file mode 100644
index 0000000000..dcb041db1f
--- /dev/null
+++ b/server/data-migrations/1742369436314-StepsV2Migration.ts
@@ -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
{
+ 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 {}
+
+ 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,
+ });
+ }
+ }
+}
diff --git a/server/src/dto/validators/tooljet-database.validator.ts b/server/src/dto/validators/tooljet-database.validator.ts
index 6ebea6bd17..164087bd0e 100644
--- a/server/src/dto/validators/tooljet-database.validator.ts
+++ b/server/src/dto/validators/tooljet-database.validator.ts
@@ -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,
+ });
+ };
+}
diff --git a/server/src/modules/apps/module.ts b/server/src/modules/apps/module.ts
index 15b5903fb2..6565c17ed1 100644
--- a/server/src/modules/apps/module.ts
+++ b/server/src/modules/apps/module.ts
@@ -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 {
@@ -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],
};
}
}
diff --git a/server/src/modules/apps/repository.ts b/server/src/modules/apps/repository.ts
index 98c76b5634..172689e303 100644
--- a/server/src/modules/apps/repository.ts
+++ b/server/src/modules/apps/repository.ts
@@ -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 {
@@ -63,4 +64,23 @@ export class AppsRepository extends Repository {
},
});
}
+
+ async findAllOrganizationApps(organizationId: string): Promise {
+ 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();
+ }
}
diff --git a/server/src/modules/apps/service.ts b/server/src/modules/apps/service.ts
index f76ce660ab..27fd03c034 100644
--- a/server/src/modules/apps/service.ts
+++ b/server/src/modules/apps/service.ts
@@ -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 {
diff --git a/server/src/modules/apps/services/app-import-export.service.ts b/server/src/modules/apps/services/app-import-export.service.ts
index 6cd6b3ce8f..16fa8289f6 100644
--- a/server/src/modules/apps/services/app-import-export.service.ts
+++ b/server/src/modules/apps/services/app-import-export.service.ts
@@ -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;
dataQueryMapping: Record;
@@ -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 => {
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);
diff --git a/server/src/modules/apps/services/component.service.ts b/server/src/modules/apps/services/component.service.ts
index a7538f5f40..fcc01e52f0 100644
--- a/server/src/modules/apps/services/component.service.ts
+++ b/server/src/modules/apps/services/component.service.ts
@@ -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);
diff --git a/server/src/modules/apps/services/widget-config/steps.js b/server/src/modules/apps/services/widget-config/steps.js
index c8b9753d9a..4400a19137 100644
--- a/server/src/modules/apps/services/widget-config/steps.js
+++ b/server/src/modules/apps/services/widget-config/steps.js
@@ -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' },
},
},
};
diff --git a/server/src/modules/apps/util.service.ts b/server/src/modules/apps/util.service.ts
index 36def10913..3db7df4a99 100644
--- a/server/src/modules/apps/util.service.ts
+++ b/server/src/modules/apps/util.service.ts
@@ -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 {
+ 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 };
+ });
+ });
+ }
}
diff --git a/server/src/modules/auth/guards/external-api-security.guard.ts b/server/src/modules/auth/guards/external-api-security.guard.ts
index 0d6ab91863..f6aced14d5 100644
--- a/server/src/modules/auth/guards/external-api-security.guard.ts
+++ b/server/src/modules/auth/guards/external-api-security.guard.ts
@@ -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('EXTERNAL_API_ACCESS_TOKEN');
diff --git a/server/src/modules/data-queries/service.ts b/server/src/modules/data-queries/service.ts
index 48aa236cee..5337c6739a 100644
--- a/server/src/modules/data-queries/service.ts
+++ b/server/src/modules/data-queries/service.ts
@@ -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);
diff --git a/server/src/modules/external-apis/constants/feature.ts b/server/src/modules/external-apis/constants/feature.ts
index 6dcccf4e70..5551dd64e0 100644
--- a/server/src/modules/external-apis/constants/feature.ts
+++ b/server/src/modules/external-apis/constants/feature.ts
@@ -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,
+ },
},
};
diff --git a/server/src/modules/external-apis/constants/index.ts b/server/src/modules/external-apis/constants/index.ts
index 8fdca84235..97030f67a7 100644
--- a/server/src/modules/external-apis/constants/index.ts
+++ b/server/src/modules/external-apis/constants/index.ts
@@ -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',
+];
diff --git a/server/src/modules/external-apis/controller.ts b/server/src/modules/external-apis/controller.ts
index 3a6f533243..7180ea23fb 100644
--- a/server/src/modules/external-apis/controller.ts
+++ b/server/src/modules/external-apis/controller.ts
@@ -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 {
diff --git a/server/src/modules/external-apis/dto/index.ts b/server/src/modules/external-apis/dto/index.ts
index 71fe51141b..9d8d6a0f9d 100644
--- a/server/src/modules/external-apis/dto/index.ts
+++ b/server/src/modules/external-apis/dto/index.ts
@@ -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;
+}
diff --git a/server/src/modules/external-apis/guards/external-api-security.guard.ts b/server/src/modules/external-apis/guards/external-api-security.guard.ts
deleted file mode 100644
index 0d6ab91863..0000000000
--- a/server/src/modules/external-apis/guards/external-api-security.guard.ts
+++ /dev/null
@@ -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('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('EXTERNAL_API_ACCESS_TOKEN');
-
- if (!authHeader || authHeader !== `Basic ${externalApiAccessToken}`) {
- throw new ForbiddenException('Unauthorized');
- }
-
- return true;
- }
-}
diff --git a/server/src/modules/external-apis/module.ts b/server/src/modules/external-apis/module.ts
index c4a209c0bc..22621363e6 100644
--- a/server/src/modules/external-apis/module.ts
+++ b/server/src/modules/external-apis/module.ts
@@ -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 {
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],
};
diff --git a/server/src/modules/external-apis/types/index.ts b/server/src/modules/external-apis/types/index.ts
index 8c782681a6..5ed8d5c323 100644
--- a/server/src/modules/external-apis/types/index.ts
+++ b/server/src/modules/external-apis/types/index.ts
@@ -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;
+ dataQueryMapping: Record;
+ appVersionMapping: Record;
+ appEnvironmentMapping: Record;
+ appDefaultEnvironmentMapping: Record;
+ pagesMapping: Record;
+ componentsMapping: Record;
+}
diff --git a/server/src/modules/organizations/interfaces/IUtilService.ts b/server/src/modules/organizations/interfaces/IUtilService.ts
new file mode 100644
index 0000000000..a6162c3cef
--- /dev/null
+++ b/server/src/modules/organizations/interfaces/IUtilService.ts
@@ -0,0 +1,3 @@
+export interface IOrganizationUtilService {
+ validateWorkspaceExists(workspaceId: string): Promise;
+}
diff --git a/server/src/modules/organizations/module.ts b/server/src/modules/organizations/module.ts
index b7432d5e4c..e533607557 100644
--- a/server/src/modules/organizations/module.ts
+++ b/server/src/modules/organizations/module.ts
@@ -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 {
@@ -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],
};
}
}
diff --git a/server/src/modules/organizations/util.service.ts b/server/src/modules/organizations/util.service.ts
new file mode 100644
index 0000000000..9d501eb7f5
--- /dev/null
+++ b/server/src/modules/organizations/util.service.ts
@@ -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}`);
+ }
+ }
+}
diff --git a/server/src/modules/roles/service.ts b/server/src/modules/roles/service.ts
index d638f10635..85bc6f87d7 100644
--- a/server/src/modules/roles/service.ts
+++ b/server/src/modules/roles/service.ts
@@ -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 {
diff --git a/server/src/modules/users/module.ts b/server/src/modules/users/module.ts
index bd91972dba..963eb08fca 100644
--- a/server/src/modules/users/module.ts
+++ b/server/src/modules/users/module.ts
@@ -16,6 +16,7 @@ export class UsersModule {
imports: [await SessionModule.register(configs)],
controllers: [UsersController],
providers: [UsersService, UserRepository, UsersUtilService, FeatureAbilityFactory],
+ exports: [UsersUtilService],
};
}
}
diff --git a/server/src/modules/users/repository.ts b/server/src/modules/users/repository.ts
index 236e8de8fa..e3417e5e50 100644
--- a/server/src/modules/users/repository.ts
+++ b/server/src/modules/users/repository.ts
@@ -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 {
await manager.upsert(UserDetails, updatableParams, conflictsPaths);
}
+ async getUserWithAdminRole(organizationId: string, manager?: EntityManager): Promise {
+ 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,
diff --git a/server/src/modules/versions/interfaces/IUtilService.ts b/server/src/modules/versions/interfaces/IUtilService.ts
index 2b384ff789..768de2afdb 100644
--- a/server/src/modules/versions/interfaces/IUtilService.ts
+++ b/server/src/modules/versions/interfaces/IUtilService.ts
@@ -3,4 +3,5 @@ import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
export interface IVersionUtilService {
updateVersion(appVersion: AppVersion, appVersionUpdateDto: AppVersionUpdateDto): Promise;
+ fetchVersions(appId: string): Promise;
}
diff --git a/server/src/modules/versions/module.ts b/server/src/modules/versions/module.ts
index 1f08dc76bb..261401a18d 100644
--- a/server/src/modules/versions/module.ts
+++ b/server/src/modules/versions/module.ts
@@ -51,6 +51,7 @@ export class VersionModule {
VersionUtilService,
FeatureAbilityFactory,
],
+ exports: [VersionUtilService],
};
}
}
diff --git a/server/src/modules/versions/util.service.ts b/server/src/modules/versions/util.service.ts
index f5723e0377..124a3c2ab3 100644
--- a/server/src/modules/versions/util.service.ts
+++ b/server/src/modules/versions/util.service.ts
@@ -72,4 +72,13 @@ export class VersionUtilService implements IVersionUtilService {
await this.versionRepository.update(appVersion.id, editableParams);
return;
}
+
+ async fetchVersions(appId: string): Promise {
+ return await this.versionRepository.find({
+ where: { appId },
+ order: {
+ createdAt: 'DESC',
+ },
+ });
+ }
}