diff --git a/frontend/package.json b/frontend/package.json index 45d94532f5..3821a370f5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -58,6 +58,7 @@ "dotenv": "^16.0.3", "draft-js": "^0.11.7", "draft-js-export-html": "^1.4.1", + "draft-js-import-html": "^1.4.1", "driver.js": "^0.9.8", "emoji-mart": "^5.5.2", "file-loader": "^6.2.0", diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js index 913d3a22df..5f362ba0b3 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js @@ -232,6 +232,7 @@ export const getAllChildComponents = (allComponents, parentId) => { const childTabId = componentParentId.split('-').at(-1); if (componentParentId === `${parentId}-${childTabId}`) { childComponent.isParentTabORCalendar = true; + childComponent.events = useStore.getState().eventsSlice.getEventsByComponentsId(componentId); childComponents.push(childComponent); // Recursively find children of the current child component const childrenOfChild = getAllChildComponents(allComponents, componentId); @@ -242,6 +243,7 @@ export const getAllChildComponents = (allComponents, parentId) => { if (componentParentId === parentId) { let childComponent = deepClone(allComponents[componentId]); childComponent.id = componentId; + childComponent.events = useStore.getState().eventsSlice.getEventsByComponentsId(componentId); childComponents.push(childComponent); // Recursively find children of the current child component diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Steps.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Steps.jsx new file mode 100644 index 0000000000..314fe8ba89 --- /dev/null +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Steps.jsx @@ -0,0 +1,538 @@ +import React, { useState, useEffect } from 'react'; +import Accordion from '@/_ui/Accordion'; +import { EventManager } from '../EventManager'; +import { renderElement } from '../Utils'; +import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; +import Popover from 'react-bootstrap/Popover'; +import List from '@/ToolJetUI/List/List'; +import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; +import useStore from '@/AppBuilder/_stores/store'; +import CodeHinter from '@/AppBuilder/CodeEditor'; +import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton'; +import ListGroup from 'react-bootstrap/ListGroup'; +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; +import SortableList from '@/_components/SortableList'; +import Trash from '@/_ui/Icon/solidIcons/Trash'; +import { shallow } from 'zustand/shallow'; +import Switch from '@/Editor/CodeBuilder/Elements/Switch'; +import { usePrevious } from '@dnd-kit/utilities'; + +export function Steps({ componentMeta, darkMode, ...restProps }) { + const { + layoutPropertyChanged, + component, + dataQueries, + paramUpdated, + currentState, + eventsChanged, + apps, + allComponents, + pages, + } = restProps; + const getResolvedValue = useStore((state) => state.getResolvedValue, shallow); + + const isDynamicOptionsEnabled = getResolvedValue(component?.component?.definition?.properties?.advanced?.value); + const variant = component?.component?.definition?.properties?.variant?.value; + const prevVariant = usePrevious(variant) + console.log("variant", component?.component?.definition); + + + const [options, setOptions] = useState([]); + const [hoveredOptionIndex, setHoveredOptionIndex] = useState(null); + let properties = []; + let additionalActions = []; + let optionsProperties = []; + + for (const [key] of Object.entries(componentMeta?.properties)) { + if (componentMeta?.properties[key]?.section === 'additionalActions') { + additionalActions.push(key); + } else if (componentMeta?.properties[key]?.accordian === 'Options') { + optionsProperties.push(key); + } else { + properties.push(key); + } + } + + // the default style of "number" & "titles" type are different for completed label + // TODO: Need to revisit this logic when text custom themes are implemented + useEffect(() => { + const completedLabelColor = component?.component?.definition?.styles?.completedLabel?.value; + if (variant !== prevVariant) { + if (variant === "numbers" && completedLabelColor === "#1B1F24") { + paramUpdated({ name: 'completedLabel' }, 'value', "#FFFFFF", 'styles', false, {}); + } else if (variant === "titles" && completedLabelColor === "#FFFFFF") { + paramUpdated({ name: 'completedLabel' }, 'value', "#1B1F24", 'styles', false, {}); + } + } + + }, [variant]) + + const getItemStyle = (isDragging, draggableStyle) => ({ + userSelect: 'none', + ...draggableStyle, + }); + + const updateAllOptionsParams = (options, props) => { + paramUpdated({ name: 'steps' }, 'value', options, 'properties', false, props); + }; + + const generateNewOptions = () => { + let found = false; + let label = ''; + let currentNumber = options.length + 1; + while (!found) { + label = `step ${currentNumber}`; + if (options.find((option) => option.name === label) === undefined) { + found = true; + } + currentNumber += 1; + } + return { + name: label, + id: currentNumber - 1, + tooltip: label, + visible: { value: '{{true}}' }, + disabled: { value: '{{false}}' }, + }; + }; + + const handleAddOption = () => { + let _option = generateNewOptions(); + const _items = [...options, _option]; + setOptions(_items); + updateAllOptionsParams(_items); + }; + const handleDeleteOption = (index) => { + const _items = options.filter((option, i) => i !== index); + setOptions(_items); + updateAllOptionsParams(_items, { isParamFromDropdownOptions: true }); + }; + + const handleLabelChange = (propertyName, value, index) => { + const _options = options.map((option, i) => { + if (i === index) { + return { + ...option, + [propertyName]: value, + }; + } + return option; + }); + setOptions(_options); + updateAllOptionsParams(_options); + }; + + const reorderOptions = async (startIndex, endIndex) => { + const result = [...options]; + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + setOptions(result); + updateAllOptionsParams(result); + }; + + const onDragEnd = ({ source, destination }) => { + if (!destination || source?.index === destination?.index) { + return; + } + reorderOptions(source.index, destination.index); + }; + + const handleOnFxPress = (active, index, key) => { + const _options = options.map((option, i) => { + if (i === index) { + return { + ...option, + [key]: { + ...option[key], + fxActive: active, + }, + }; + } + return option; + }); + setOptions(_options); + updateAllOptionsParams(_options); + }; + + const _renderOverlay = (item, index) => { + return ( + + +
+ + handleLabelChange('id', value, index)} + /> +
+
+ + handleLabelChange('name', value, index)} + /> +
+
+ + handleLabelChange('tooltip', value, index)} + /> +
+
+ + handleLabelChange( + 'visible', + { + value, + }, + index + ) + } + paramName={'visible'} + onFxPress={(active) => handleOnFxPress(active, index, 'visible')} + fxActive={item?.visible?.fxActive} + fieldMeta={{ + type: 'toggle', + displayName: 'Make editable', + }} + paramType={'toggle'} + /> +
+
+ handleLabelChange('disabled', { value }, index)} + onFxPress={(active) => handleOnFxPress(active, index, 'disabled')} + fxActive={item?.disabled?.fxActive} + fieldMeta={{ + type: 'toggle', + displayName: 'Make editable', + }} + paramType={'toggle'} + /> +
+
+
+ ); + }; + const _renderOptions = () => { + return ( + + { + onDragEnd(result); + }} + > + + {({ innerRef, droppableProps, placeholder }) => ( +
+ {options?.map((item, index) => { + return ( + + {(provided, snapshot) => ( +
+ +
+ setHoveredOptionIndex(index)} + onMouseLeave={() => setHoveredOptionIndex(null)} + {...restProps} + > +
+
+ +
+
+ {getResolvedValue(item.name)} +
+
+ {index === hoveredOptionIndex && ( + { + e.stopPropagation(); + handleDeleteOption(index); + }} + > + + + + + )} +
+
+
+
+
+
+ )} +
+ ); + })} + {placeholder} +
+ )} +
+
+ + Add new option + +
+ ); + }; + + const isDynamicStepsEnabled = getResolvedValue(component?.component?.definition?.properties?.advanced?.value); + useEffect(() => { + setOptions(constructSteps()); + }, [component?.id, isDynamicStepsEnabled]); + + const constructSteps = () => { + try { + let optionsValue = isDynamicOptionsEnabled + ? component?.component?.definition?.properties?.schema?.value + : component?.component?.definition?.properties?.steps?.value; + let options = []; + + if (isDynamicOptionsEnabled || typeof optionsValue === 'string') { + options = getResolvedValue(optionsValue); + } else { + options = optionsValue?.map((option) => option); + } + return options.map((option) => { + const newOption = { ...option }; + + Object.keys(option).forEach((key) => { + if (typeof option[key]?.value === 'boolean') { + newOption[key]['value'] = `{{${option[key]?.value}}}`; + } + }); + + if (!('visible' in newOption)) { + newOption['visible'] = { value: '{{true}}' }; + } + return newOption; + }); + } catch (error) { + return []; + } + }; + + let items = []; + + items.push({ + title: 'Steps', + isOpen: true, + children: ( + <> + {properties + .filter((property) => !optionsProperties.includes(property)) + ?.map((property) => { + if (property === 'steps') { + return ( + <> + {renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + 'advanced', + 'properties', + currentState, + allComponents + )} + {isDynamicStepsEnabled + ? renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + 'schema', + 'properties', + currentState, + allComponents + ) + : _renderOptions()} + + ); + } + // else if (property === 'variant') { + // return renderTest( + // component, + // componentMeta, + // paramUpdated, + // dataQueries, + // 'variant', + // 'properties', + // currentState, + // allComponents, + // handleLabelChange + // ); + // } + return renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + property, + 'properties', + currentState, + allComponents, + darkMode + ); + })} + + ), + }); + + items.push({ + title: 'Events', + isOpen: true, + children: ( + + ), + }); + items.push({ + title: `Additional Actions`, + isOpen: true, + children: additionalActions.map((property) => { + return renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + property, + 'properties', + currentState, + allComponents, + darkMode, + componentMeta.properties?.[property]?.placeholder + ); + }), + }); + + items.push({ + title: 'Devices', + isOpen: true, + children: ( + <> + {renderElement( + component, + componentMeta, + layoutPropertyChanged, + dataQueries, + 'showOnDesktop', + 'others', + currentState, + allComponents + )} + {renderElement( + component, + componentMeta, + layoutPropertyChanged, + dataQueries, + 'showOnMobile', + 'others', + currentState, + allComponents + )} + + ), + }); + + return ; +} + +function renderTest(...props) { + const [ + component, + componentMeta, + paramUpdated, + dataQueries, + param, + paramType, + currentState, + components = {}, + darkMode = false, + placeholder = '', + validationFn, + ] = props; + const value = componentMeta?.definition?.properties?.variant?.value; + return ( +
+ { + paramUpdated({ name: 'variant' }, 'value', e, 'properties', false, props); + }} + meta={{ + ...componentMeta.properties[param], + fullWidth: true, + }} + paramName={param} + isIcon={false} + component={component.component.definition.name} + /> +
+ ); +} diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx index 0568b85ccd..135f2b450b 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Table/ColumnManager/PropertiesTabElements.jsx @@ -255,7 +255,7 @@ export const PropertiesTabElements = ({ paramType="properties" /> - {resolveReferences(column?.isEditable) && ( + {(column?.fxActiveFields?.includes('isEditable') || resolveReferences(column?.isEditable)) && ( { - return { name: action.name, value: action.id }; + let groupedOptions = ActionTypes.reduce((acc, action) => { + const groupName = action.group; + + if (!acc[groupName]) { + acc[groupName] = []; + } + + acc[groupName].push({ + label: action.name, + value: action.id, + }); + + return acc; + }, {}); + + let actionOptions = Object.keys(groupedOptions).map((groupName) => { + return { label: groupName, options: groupedOptions[groupName] }; }); let checkIfClicksAreInsideOf = document.querySelector('.cm-completionListIncompleteBottom'); @@ -127,6 +144,46 @@ export const EventManager = ({ }), }; + const actionStyles = { + ...styles, + menuList: (base) => ({ + ...base, + padding: '8px 0 8px 8px', + '&::-webkit-scrollbar': { + width: '10px', + }, + '&::-webkit-scrollbar-track': { + background: 'transparent', + }, + '&::-webkit-scrollbar-thumb': { + background: '#E4E7EB', + border: '1px solid transparent', + backgroundClip: 'content-box', + }, + '&::-webkit-scrollbar-thumb:hover': { + background: '#E4E7EB !important', + border: '1px solid transparent !important', + backgroundClip: 'content-box !important', + }, + '&:hover': { + '&::-webkit-scrollbar-thumb': { + background: '#E4E7EB !important', + border: '1px solid transparent !important', + backgroundClip: 'content-box !important', + }, + }, + }), + group: (base) => ({ + ...base, + padding: 0, + }), + groupHeading: (base) => ({ + ...base, + margin: 0, + padding: '0', + }), + }; + const actionLookup = Object.fromEntries(ActionTypes.map((actionType) => [actionType.id, actionType])); let alertTypes = [ @@ -397,6 +454,29 @@ export const EventManager = ({ return defaultValue; }; + const formatGroupLabel = (data) => { + if (data.label === 'run-action') return; + return ( +
+ ); + }; + + const CustomOption = (props) => { + return ( + +
+
+ {props.isSelected && } +
+ {props.label} +
+
+ ); + }; + function eventPopover(event, index) { return ( group.options) + .find((option) => option.value === event.actionId)} + components={{ Option: CustomOption }} search={false} onChange={(value) => handlerChanged(index, 'actionId', value)} placeholder={t('globals.select', 'Select') + '...'} - styles={styles} + styles={actionStyles} useMenuPortal={false} useCustomStyles={true} + formatGroupLabel={formatGroupLabel} /> diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx index e101069fdb..07b53c4454 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Inspector.jsx @@ -36,6 +36,7 @@ import Inspect from '@/_ui/Icon/solidIcons/Inspect'; import classNames from 'classnames'; import { EMPTY_ARRAY } from '@/_stores/editorStore'; import { Select } from './Components/Select'; +import { Steps } from './Components/Steps.jsx'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; import useStore from '@/AppBuilder/_stores/store'; // import { componentTypes } from '@/Editor/WidgetManager/components'; @@ -90,6 +91,7 @@ const NEW_REVAMPED_COMPONENTS = [ 'VerticalDivider', 'ModalV2', 'Link', + 'Steps', ]; export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selectedComponentId }) => { @@ -539,8 +541,8 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte componentMeta.displayName === 'Toggle Switch (Legacy)' ? 'Toggle (Legacy)' : componentMeta.displayName === 'Toggle Switch' - ? 'Toggle Switch' - : componentMeta.component, + ? 'Toggle Switch' + : componentMeta.component, })} @@ -740,6 +742,8 @@ const GetAccordion = React.memo( case 'DatePickerV2': case 'TimePicker': return ; + case 'Steps': + return ; case 'PhoneInput': return ; case 'CurrencyInput': diff --git a/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx b/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx index 9ce3bbd4ce..5b0868c128 100644 --- a/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx +++ b/frontend/src/AppBuilder/RightSideBar/WidgetBox/WidgetBox.jsx @@ -14,6 +14,9 @@ const NEW_WIDGETS = [ 'TimePicker', 'ModalV2', 'TextArea', + 'EmailInput', + 'PhoneInput', + 'CurrencyInput', ]; export const WidgetBox = ({ component, darkMode }) => { diff --git a/frontend/src/AppBuilder/Viewer/Viewer.jsx b/frontend/src/AppBuilder/Viewer/Viewer.jsx index af820fca13..b546bc7205 100644 --- a/frontend/src/AppBuilder/Viewer/Viewer.jsx +++ b/frontend/src/AppBuilder/Viewer/Viewer.jsx @@ -168,6 +168,7 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod showViewerNavigation={!isPagesSidebarHidden} handleAppEnvironmentChanged={handleAppEnvironmentChanged} changeToDarkMode={changeToDarkMode} + switchPage={switchPage} /> )}
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', + }, + }); + } }