mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
Merge branch 'appbuilder/sprint-12' into fix/query-panel-overflow
This commit is contained in:
commit
6fa8457d95
45 changed files with 2188 additions and 686 deletions
|
|
@ -58,6 +58,7 @@
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"draft-js": "^0.11.7",
|
"draft-js": "^0.11.7",
|
||||||
"draft-js-export-html": "^1.4.1",
|
"draft-js-export-html": "^1.4.1",
|
||||||
|
"draft-js-import-html": "^1.4.1",
|
||||||
"driver.js": "^0.9.8",
|
"driver.js": "^0.9.8",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,7 @@ export const getAllChildComponents = (allComponents, parentId) => {
|
||||||
const childTabId = componentParentId.split('-').at(-1);
|
const childTabId = componentParentId.split('-').at(-1);
|
||||||
if (componentParentId === `${parentId}-${childTabId}`) {
|
if (componentParentId === `${parentId}-${childTabId}`) {
|
||||||
childComponent.isParentTabORCalendar = true;
|
childComponent.isParentTabORCalendar = true;
|
||||||
|
childComponent.events = useStore.getState().eventsSlice.getEventsByComponentsId(componentId);
|
||||||
childComponents.push(childComponent);
|
childComponents.push(childComponent);
|
||||||
// Recursively find children of the current child component
|
// Recursively find children of the current child component
|
||||||
const childrenOfChild = getAllChildComponents(allComponents, componentId);
|
const childrenOfChild = getAllChildComponents(allComponents, componentId);
|
||||||
|
|
@ -242,6 +243,7 @@ export const getAllChildComponents = (allComponents, parentId) => {
|
||||||
if (componentParentId === parentId) {
|
if (componentParentId === parentId) {
|
||||||
let childComponent = deepClone(allComponents[componentId]);
|
let childComponent = deepClone(allComponents[componentId]);
|
||||||
childComponent.id = componentId;
|
childComponent.id = componentId;
|
||||||
|
childComponent.events = useStore.getState().eventsSlice.getEventsByComponentsId(componentId);
|
||||||
childComponents.push(childComponent);
|
childComponents.push(childComponent);
|
||||||
|
|
||||||
// Recursively find children of the current child component
|
// Recursively find children of the current child component
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,538 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import Accordion from '@/_ui/Accordion';
|
||||||
|
import { EventManager } from '../EventManager';
|
||||||
|
import { renderElement } from '../Utils';
|
||||||
|
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||||
|
import Popover from 'react-bootstrap/Popover';
|
||||||
|
import List from '@/ToolJetUI/List/List';
|
||||||
|
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
||||||
|
import useStore from '@/AppBuilder/_stores/store';
|
||||||
|
import CodeHinter from '@/AppBuilder/CodeEditor';
|
||||||
|
import AddNewButton from '@/ToolJetUI/Buttons/AddNewButton/AddNewButton';
|
||||||
|
import ListGroup from 'react-bootstrap/ListGroup';
|
||||||
|
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||||
|
import SortableList from '@/_components/SortableList';
|
||||||
|
import Trash from '@/_ui/Icon/solidIcons/Trash';
|
||||||
|
import { shallow } from 'zustand/shallow';
|
||||||
|
import Switch from '@/Editor/CodeBuilder/Elements/Switch';
|
||||||
|
import { usePrevious } from '@dnd-kit/utilities';
|
||||||
|
|
||||||
|
export function Steps({ componentMeta, darkMode, ...restProps }) {
|
||||||
|
const {
|
||||||
|
layoutPropertyChanged,
|
||||||
|
component,
|
||||||
|
dataQueries,
|
||||||
|
paramUpdated,
|
||||||
|
currentState,
|
||||||
|
eventsChanged,
|
||||||
|
apps,
|
||||||
|
allComponents,
|
||||||
|
pages,
|
||||||
|
} = restProps;
|
||||||
|
const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
|
||||||
|
|
||||||
|
const isDynamicOptionsEnabled = getResolvedValue(component?.component?.definition?.properties?.advanced?.value);
|
||||||
|
const variant = component?.component?.definition?.properties?.variant?.value;
|
||||||
|
const prevVariant = usePrevious(variant)
|
||||||
|
console.log("variant", component?.component?.definition);
|
||||||
|
|
||||||
|
|
||||||
|
const [options, setOptions] = useState([]);
|
||||||
|
const [hoveredOptionIndex, setHoveredOptionIndex] = useState(null);
|
||||||
|
let properties = [];
|
||||||
|
let additionalActions = [];
|
||||||
|
let optionsProperties = [];
|
||||||
|
|
||||||
|
for (const [key] of Object.entries(componentMeta?.properties)) {
|
||||||
|
if (componentMeta?.properties[key]?.section === 'additionalActions') {
|
||||||
|
additionalActions.push(key);
|
||||||
|
} else if (componentMeta?.properties[key]?.accordian === 'Options') {
|
||||||
|
optionsProperties.push(key);
|
||||||
|
} else {
|
||||||
|
properties.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the default style of "number" & "titles" type are different for completed label
|
||||||
|
// TODO: Need to revisit this logic when text custom themes are implemented
|
||||||
|
useEffect(() => {
|
||||||
|
const completedLabelColor = component?.component?.definition?.styles?.completedLabel?.value;
|
||||||
|
if (variant !== prevVariant) {
|
||||||
|
if (variant === "numbers" && completedLabelColor === "#1B1F24") {
|
||||||
|
paramUpdated({ name: 'completedLabel' }, 'value', "#FFFFFF", 'styles', false, {});
|
||||||
|
} else if (variant === "titles" && completedLabelColor === "#FFFFFF") {
|
||||||
|
paramUpdated({ name: 'completedLabel' }, 'value', "#1B1F24", 'styles', false, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [variant])
|
||||||
|
|
||||||
|
const getItemStyle = (isDragging, draggableStyle) => ({
|
||||||
|
userSelect: 'none',
|
||||||
|
...draggableStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateAllOptionsParams = (options, props) => {
|
||||||
|
paramUpdated({ name: 'steps' }, 'value', options, 'properties', false, props);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateNewOptions = () => {
|
||||||
|
let found = false;
|
||||||
|
let label = '';
|
||||||
|
let currentNumber = options.length + 1;
|
||||||
|
while (!found) {
|
||||||
|
label = `step ${currentNumber}`;
|
||||||
|
if (options.find((option) => option.name === label) === undefined) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
currentNumber += 1;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: label,
|
||||||
|
id: currentNumber - 1,
|
||||||
|
tooltip: label,
|
||||||
|
visible: { value: '{{true}}' },
|
||||||
|
disabled: { value: '{{false}}' },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddOption = () => {
|
||||||
|
let _option = generateNewOptions();
|
||||||
|
const _items = [...options, _option];
|
||||||
|
setOptions(_items);
|
||||||
|
updateAllOptionsParams(_items);
|
||||||
|
};
|
||||||
|
const handleDeleteOption = (index) => {
|
||||||
|
const _items = options.filter((option, i) => i !== index);
|
||||||
|
setOptions(_items);
|
||||||
|
updateAllOptionsParams(_items, { isParamFromDropdownOptions: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLabelChange = (propertyName, value, index) => {
|
||||||
|
const _options = options.map((option, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
[propertyName]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return option;
|
||||||
|
});
|
||||||
|
setOptions(_options);
|
||||||
|
updateAllOptionsParams(_options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reorderOptions = async (startIndex, endIndex) => {
|
||||||
|
const result = [...options];
|
||||||
|
const [removed] = result.splice(startIndex, 1);
|
||||||
|
result.splice(endIndex, 0, removed);
|
||||||
|
setOptions(result);
|
||||||
|
updateAllOptionsParams(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = ({ source, destination }) => {
|
||||||
|
if (!destination || source?.index === destination?.index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reorderOptions(source.index, destination.index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnFxPress = (active, index, key) => {
|
||||||
|
const _options = options.map((option, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
[key]: {
|
||||||
|
...option[key],
|
||||||
|
fxActive: active,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return option;
|
||||||
|
});
|
||||||
|
setOptions(_options);
|
||||||
|
updateAllOptionsParams(_options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _renderOverlay = (item, index) => {
|
||||||
|
return (
|
||||||
|
<Popover className={`${darkMode && 'dark-theme theme-dark'}`} style={{ minWidth: '248px' }}>
|
||||||
|
<Popover.Body>
|
||||||
|
<div className="field mb-3" data-cy={`input-and-label-column-name`}>
|
||||||
|
<label data-cy={`label-column-name`} className="font-weight-500 mb-1 font-size-12">
|
||||||
|
{'Id'}
|
||||||
|
</label>
|
||||||
|
<CodeHinter
|
||||||
|
type={'basic'}
|
||||||
|
initialValue={item?.id + ''}
|
||||||
|
theme={darkMode ? 'monokai' : 'default'}
|
||||||
|
mode="javascript"
|
||||||
|
lineNumbers={false}
|
||||||
|
placeholder={'Option label'}
|
||||||
|
onChange={(value) => handleLabelChange('id', value, index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="field mb-3" data-cy={`input-and-label-column-name`}>
|
||||||
|
<label data-cy={`label-column-name`} className="font-weight-500 mb-1 font-size-12">
|
||||||
|
{'Label'}
|
||||||
|
</label>
|
||||||
|
<CodeHinter
|
||||||
|
type={'basic'}
|
||||||
|
initialValue={item?.name}
|
||||||
|
theme={darkMode ? 'monokai' : 'default'}
|
||||||
|
mode="javascript"
|
||||||
|
lineNumbers={false}
|
||||||
|
placeholder={'Option label'}
|
||||||
|
onChange={(value) => handleLabelChange('name', value, index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="field mb-3" data-cy={`input-and-label-column-name`}>
|
||||||
|
<label data-cy={`label-column-name`} className="font-weight-500 mb-1 font-size-12">
|
||||||
|
{'Tooltip'}
|
||||||
|
</label>
|
||||||
|
<CodeHinter
|
||||||
|
type={'basic'}
|
||||||
|
initialValue={item?.tooltip + ''}
|
||||||
|
theme={darkMode ? 'monokai' : 'default'}
|
||||||
|
mode="javascript"
|
||||||
|
lineNumbers={false}
|
||||||
|
placeholder={'Tooltip'}
|
||||||
|
onChange={(value) => handleLabelChange('tooltip', value, index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="field mb-2" data-cy={`input-and-label-column-name`}>
|
||||||
|
<CodeHinter
|
||||||
|
initialValue={item?.visible?.value}
|
||||||
|
theme={darkMode ? 'monokai' : 'default'}
|
||||||
|
mode="javascript"
|
||||||
|
lineNumbers={false}
|
||||||
|
component={component}
|
||||||
|
type={'fxEditor'}
|
||||||
|
paramLabel={'Visibility'}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleLabelChange(
|
||||||
|
'visible',
|
||||||
|
{
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
paramName={'visible'}
|
||||||
|
onFxPress={(active) => handleOnFxPress(active, index, 'visible')}
|
||||||
|
fxActive={item?.visible?.fxActive}
|
||||||
|
fieldMeta={{
|
||||||
|
type: 'toggle',
|
||||||
|
displayName: 'Make editable',
|
||||||
|
}}
|
||||||
|
paramType={'toggle'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="field" data-cy={`input-and-label-column-name`}>
|
||||||
|
<CodeHinter
|
||||||
|
initialValue={item?.disabled?.value}
|
||||||
|
theme={darkMode ? 'monokai' : 'default'}
|
||||||
|
mode="javascript"
|
||||||
|
lineNumbers={false}
|
||||||
|
component={component}
|
||||||
|
type={'fxEditor'}
|
||||||
|
paramLabel={'Disable'}
|
||||||
|
paramName={'disable'}
|
||||||
|
onChange={(value) => handleLabelChange('disabled', { value }, index)}
|
||||||
|
onFxPress={(active) => handleOnFxPress(active, index, 'disabled')}
|
||||||
|
fxActive={item?.disabled?.fxActive}
|
||||||
|
fieldMeta={{
|
||||||
|
type: 'toggle',
|
||||||
|
displayName: 'Make editable',
|
||||||
|
}}
|
||||||
|
paramType={'toggle'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Popover.Body>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const _renderOptions = () => {
|
||||||
|
return (
|
||||||
|
<List style={{ marginBottom: '20px' }}>
|
||||||
|
<DragDropContext
|
||||||
|
onDragEnd={(result) => {
|
||||||
|
onDragEnd(result);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Droppable droppableId="droppable">
|
||||||
|
{({ innerRef, droppableProps, placeholder }) => (
|
||||||
|
<div className="w-100" {...droppableProps} ref={innerRef}>
|
||||||
|
{options?.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<Draggable key={item.name} draggableId={item.name} index={index}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
|
||||||
|
>
|
||||||
|
<OverlayTrigger
|
||||||
|
trigger="click"
|
||||||
|
placement="left"
|
||||||
|
rootClose
|
||||||
|
overlay={_renderOverlay(item, index)}
|
||||||
|
>
|
||||||
|
<div key={item.name + item.id}>
|
||||||
|
<ListGroup.Item
|
||||||
|
style={{ marginBottom: '8px', backgroundColor: 'var(--slate3)' }}
|
||||||
|
onMouseEnter={() => setHoveredOptionIndex(index)}
|
||||||
|
onMouseLeave={() => setHoveredOptionIndex(null)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-auto d-flex align-items-center">
|
||||||
|
<SortableList.DragHandle show />
|
||||||
|
</div>
|
||||||
|
<div className="col text-truncate cursor-pointer" style={{ padding: '0px' }}>
|
||||||
|
{getResolvedValue(item.name)}
|
||||||
|
</div>
|
||||||
|
<div className="col-auto">
|
||||||
|
{index === hoveredOptionIndex && (
|
||||||
|
<ButtonSolid
|
||||||
|
variant="danger"
|
||||||
|
size="xs"
|
||||||
|
className={'delete-icon-btn'}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteOption(index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="d-flex">
|
||||||
|
<Trash fill={'var(--tomato9)'} width={12} />
|
||||||
|
</span>
|
||||||
|
</ButtonSolid>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ListGroup.Item>
|
||||||
|
</div>
|
||||||
|
</OverlayTrigger>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
<AddNewButton onClick={handleAddOption} dataCy="add-new-dropdown-option" className="mt-0">
|
||||||
|
Add new option
|
||||||
|
</AddNewButton>
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDynamicStepsEnabled = getResolvedValue(component?.component?.definition?.properties?.advanced?.value);
|
||||||
|
useEffect(() => {
|
||||||
|
setOptions(constructSteps());
|
||||||
|
}, [component?.id, isDynamicStepsEnabled]);
|
||||||
|
|
||||||
|
const constructSteps = () => {
|
||||||
|
try {
|
||||||
|
let optionsValue = isDynamicOptionsEnabled
|
||||||
|
? component?.component?.definition?.properties?.schema?.value
|
||||||
|
: component?.component?.definition?.properties?.steps?.value;
|
||||||
|
let options = [];
|
||||||
|
|
||||||
|
if (isDynamicOptionsEnabled || typeof optionsValue === 'string') {
|
||||||
|
options = getResolvedValue(optionsValue);
|
||||||
|
} else {
|
||||||
|
options = optionsValue?.map((option) => option);
|
||||||
|
}
|
||||||
|
return options.map((option) => {
|
||||||
|
const newOption = { ...option };
|
||||||
|
|
||||||
|
Object.keys(option).forEach((key) => {
|
||||||
|
if (typeof option[key]?.value === 'boolean') {
|
||||||
|
newOption[key]['value'] = `{{${option[key]?.value}}}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!('visible' in newOption)) {
|
||||||
|
newOption['visible'] = { value: '{{true}}' };
|
||||||
|
}
|
||||||
|
return newOption;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let items = [];
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
title: 'Steps',
|
||||||
|
isOpen: true,
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
{properties
|
||||||
|
.filter((property) => !optionsProperties.includes(property))
|
||||||
|
?.map((property) => {
|
||||||
|
if (property === 'steps') {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderElement(
|
||||||
|
component,
|
||||||
|
componentMeta,
|
||||||
|
paramUpdated,
|
||||||
|
dataQueries,
|
||||||
|
'advanced',
|
||||||
|
'properties',
|
||||||
|
currentState,
|
||||||
|
allComponents
|
||||||
|
)}
|
||||||
|
{isDynamicStepsEnabled
|
||||||
|
? renderElement(
|
||||||
|
component,
|
||||||
|
componentMeta,
|
||||||
|
paramUpdated,
|
||||||
|
dataQueries,
|
||||||
|
'schema',
|
||||||
|
'properties',
|
||||||
|
currentState,
|
||||||
|
allComponents
|
||||||
|
)
|
||||||
|
: _renderOptions()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// else if (property === 'variant') {
|
||||||
|
// return renderTest(
|
||||||
|
// component,
|
||||||
|
// componentMeta,
|
||||||
|
// paramUpdated,
|
||||||
|
// dataQueries,
|
||||||
|
// 'variant',
|
||||||
|
// 'properties',
|
||||||
|
// currentState,
|
||||||
|
// allComponents,
|
||||||
|
// handleLabelChange
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
return renderElement(
|
||||||
|
component,
|
||||||
|
componentMeta,
|
||||||
|
paramUpdated,
|
||||||
|
dataQueries,
|
||||||
|
property,
|
||||||
|
'properties',
|
||||||
|
currentState,
|
||||||
|
allComponents,
|
||||||
|
darkMode
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
title: 'Events',
|
||||||
|
isOpen: true,
|
||||||
|
children: (
|
||||||
|
<EventManager
|
||||||
|
sourceId={component?.id}
|
||||||
|
eventSourceType="component"
|
||||||
|
eventMetaDefinition={componentMeta}
|
||||||
|
dataQueries={dataQueries}
|
||||||
|
components={allComponents}
|
||||||
|
eventsChanged={eventsChanged}
|
||||||
|
apps={apps}
|
||||||
|
darkMode={darkMode}
|
||||||
|
pages={pages}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
items.push({
|
||||||
|
title: `Additional Actions`,
|
||||||
|
isOpen: true,
|
||||||
|
children: additionalActions.map((property) => {
|
||||||
|
return renderElement(
|
||||||
|
component,
|
||||||
|
componentMeta,
|
||||||
|
paramUpdated,
|
||||||
|
dataQueries,
|
||||||
|
property,
|
||||||
|
'properties',
|
||||||
|
currentState,
|
||||||
|
allComponents,
|
||||||
|
darkMode,
|
||||||
|
componentMeta.properties?.[property]?.placeholder
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
title: 'Devices',
|
||||||
|
isOpen: true,
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
{renderElement(
|
||||||
|
component,
|
||||||
|
componentMeta,
|
||||||
|
layoutPropertyChanged,
|
||||||
|
dataQueries,
|
||||||
|
'showOnDesktop',
|
||||||
|
'others',
|
||||||
|
currentState,
|
||||||
|
allComponents
|
||||||
|
)}
|
||||||
|
{renderElement(
|
||||||
|
component,
|
||||||
|
componentMeta,
|
||||||
|
layoutPropertyChanged,
|
||||||
|
dataQueries,
|
||||||
|
'showOnMobile',
|
||||||
|
'others',
|
||||||
|
currentState,
|
||||||
|
allComponents
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return <Accordion items={items} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTest(...props) {
|
||||||
|
const [
|
||||||
|
component,
|
||||||
|
componentMeta,
|
||||||
|
paramUpdated,
|
||||||
|
dataQueries,
|
||||||
|
param,
|
||||||
|
paramType,
|
||||||
|
currentState,
|
||||||
|
components = {},
|
||||||
|
darkMode = false,
|
||||||
|
placeholder = '',
|
||||||
|
validationFn,
|
||||||
|
] = props;
|
||||||
|
const value = componentMeta?.definition?.properties?.variant?.value;
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<Switch
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
paramUpdated({ name: 'variant' }, 'value', e, 'properties', false, props);
|
||||||
|
}}
|
||||||
|
meta={{
|
||||||
|
...componentMeta.properties[param],
|
||||||
|
fullWidth: true,
|
||||||
|
}}
|
||||||
|
paramName={param}
|
||||||
|
isIcon={false}
|
||||||
|
component={component.component.definition.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -255,7 +255,7 @@ export const PropertiesTabElements = ({
|
||||||
paramType="properties"
|
paramType="properties"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{resolveReferences(column?.isEditable) && (
|
{(column?.fxActiveFields?.includes('isEditable') || resolveReferences(column?.isEditable)) && (
|
||||||
<ValidationProperties
|
<ValidationProperties
|
||||||
column={column}
|
column={column}
|
||||||
index={index}
|
index={index}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ import { useEventActions, useEvents } from '@/AppBuilder/_stores/slices/eventsSl
|
||||||
import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup';
|
import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup';
|
||||||
import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
|
import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
|
||||||
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
|
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
|
||||||
|
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||||
|
import { components as selectComponents } from 'react-select';
|
||||||
|
|
||||||
export const EventManager = ({
|
export const EventManager = ({
|
||||||
sourceId,
|
sourceId,
|
||||||
|
|
@ -104,8 +106,23 @@ export const EventManager = ({
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [JSON.stringify(currentEvents)]);
|
}, [JSON.stringify(currentEvents)]);
|
||||||
|
|
||||||
let actionOptions = ActionTypes.map((action) => {
|
let groupedOptions = ActionTypes.reduce((acc, action) => {
|
||||||
return { name: action.name, value: action.id };
|
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');
|
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]));
|
const actionLookup = Object.fromEntries(ActionTypes.map((actionType) => [actionType.id, actionType]));
|
||||||
|
|
||||||
let alertTypes = [
|
let alertTypes = [
|
||||||
|
|
@ -397,6 +454,29 @@ export const EventManager = ({
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatGroupLabel = (data) => {
|
||||||
|
if (data.label === 'run-action') return;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="tw-border-x-0 tw-border-t-0 tw-border-b-[0.5px] tw-border-solid tw-my-[4px]"
|
||||||
|
style={{ borderColor: 'var(--border-weak)' }}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomOption = (props) => {
|
||||||
|
return (
|
||||||
|
<selectComponents.Option {...props}>
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<div style={{ width: '16px', marginRight: '6px' }}>
|
||||||
|
{props.isSelected && <SolidIcon name="tickv3" width="16px" height="16px" />}
|
||||||
|
</div>
|
||||||
|
<span>{props.label}</span>
|
||||||
|
</div>
|
||||||
|
</selectComponents.Option>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function eventPopover(event, index) {
|
function eventPopover(event, index) {
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
|
|
@ -436,13 +516,17 @@ export const EventManager = ({
|
||||||
<Select
|
<Select
|
||||||
className={`${darkMode ? 'select-search-dark' : 'select-search'} w-100`}
|
className={`${darkMode ? 'select-search-dark' : 'select-search'} w-100`}
|
||||||
options={actionOptions}
|
options={actionOptions}
|
||||||
value={event.actionId}
|
value={actionOptions
|
||||||
|
.flatMap((group) => group.options)
|
||||||
|
.find((option) => option.value === event.actionId)}
|
||||||
|
components={{ Option: CustomOption }}
|
||||||
search={false}
|
search={false}
|
||||||
onChange={(value) => handlerChanged(index, 'actionId', value)}
|
onChange={(value) => handlerChanged(index, 'actionId', value)}
|
||||||
placeholder={t('globals.select', 'Select') + '...'}
|
placeholder={t('globals.select', 'Select') + '...'}
|
||||||
styles={styles}
|
styles={actionStyles}
|
||||||
useMenuPortal={false}
|
useMenuPortal={false}
|
||||||
useCustomStyles={true}
|
useCustomStyles={true}
|
||||||
|
formatGroupLabel={formatGroupLabel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import Inspect from '@/_ui/Icon/solidIcons/Inspect';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { EMPTY_ARRAY } from '@/_stores/editorStore';
|
import { EMPTY_ARRAY } from '@/_stores/editorStore';
|
||||||
import { Select } from './Components/Select';
|
import { Select } from './Components/Select';
|
||||||
|
import { Steps } from './Components/Steps.jsx';
|
||||||
import { deepClone } from '@/_helpers/utilities/utils.helpers';
|
import { deepClone } from '@/_helpers/utilities/utils.helpers';
|
||||||
import useStore from '@/AppBuilder/_stores/store';
|
import useStore from '@/AppBuilder/_stores/store';
|
||||||
// import { componentTypes } from '@/Editor/WidgetManager/components';
|
// import { componentTypes } from '@/Editor/WidgetManager/components';
|
||||||
|
|
@ -90,6 +91,7 @@ const NEW_REVAMPED_COMPONENTS = [
|
||||||
'VerticalDivider',
|
'VerticalDivider',
|
||||||
'ModalV2',
|
'ModalV2',
|
||||||
'Link',
|
'Link',
|
||||||
|
'Steps',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selectedComponentId }) => {
|
export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selectedComponentId }) => {
|
||||||
|
|
@ -539,8 +541,8 @@ export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selecte
|
||||||
componentMeta.displayName === 'Toggle Switch (Legacy)'
|
componentMeta.displayName === 'Toggle Switch (Legacy)'
|
||||||
? 'Toggle (Legacy)'
|
? 'Toggle (Legacy)'
|
||||||
: componentMeta.displayName === 'Toggle Switch'
|
: componentMeta.displayName === 'Toggle Switch'
|
||||||
? 'Toggle Switch'
|
? 'Toggle Switch'
|
||||||
: componentMeta.component,
|
: componentMeta.component,
|
||||||
})}
|
})}
|
||||||
</small>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -740,6 +742,8 @@ const GetAccordion = React.memo(
|
||||||
case 'DatePickerV2':
|
case 'DatePickerV2':
|
||||||
case 'TimePicker':
|
case 'TimePicker':
|
||||||
return <DatetimePickerV2 {...restProps} componentName={componentName} />;
|
return <DatetimePickerV2 {...restProps} componentName={componentName} />;
|
||||||
|
case 'Steps':
|
||||||
|
return <Steps {...restProps} />;
|
||||||
case 'PhoneInput':
|
case 'PhoneInput':
|
||||||
return <PhoneInput {...restProps} />;
|
return <PhoneInput {...restProps} />;
|
||||||
case 'CurrencyInput':
|
case 'CurrencyInput':
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ const NEW_WIDGETS = [
|
||||||
'TimePicker',
|
'TimePicker',
|
||||||
'ModalV2',
|
'ModalV2',
|
||||||
'TextArea',
|
'TextArea',
|
||||||
|
'EmailInput',
|
||||||
|
'PhoneInput',
|
||||||
|
'CurrencyInput',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const WidgetBox = ({ component, darkMode }) => {
|
export const WidgetBox = ({ component, darkMode }) => {
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,7 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod
|
||||||
showViewerNavigation={!isPagesSidebarHidden}
|
showViewerNavigation={!isPagesSidebarHidden}
|
||||||
handleAppEnvironmentChanged={handleAppEnvironmentChanged}
|
handleAppEnvironmentChanged={handleAppEnvironmentChanged}
|
||||||
changeToDarkMode={changeToDarkMode}
|
changeToDarkMode={changeToDarkMode}
|
||||||
|
switchPage={switchPage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="sub-section">
|
<div className="sub-section">
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,38 @@ export const stepsConfig = {
|
||||||
description: 'Step-by-step navigation aid',
|
description: 'Step-by-step navigation aid',
|
||||||
component: 'Steps',
|
component: 'Steps',
|
||||||
properties: {
|
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: {
|
steps: {
|
||||||
type: 'code',
|
type: 'code',
|
||||||
displayName: 'Steps',
|
displayName: '',
|
||||||
|
showLabel: false,
|
||||||
validation: {
|
validation: {
|
||||||
schema: {
|
schema: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
element: { type: 'object', object: { id: { type: 'number' } } },
|
element: { type: 'object' },
|
||||||
},
|
},
|
||||||
defaultValue: `[{ name: 'step 1'}, {name: 'step 2'}]`,
|
defaultValue: `[{ name: 'step 1'}, {name: 'step 2'}]`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
currentStep: {
|
|
||||||
type: 'code',
|
|
||||||
displayName: 'Current step',
|
|
||||||
validation: {
|
|
||||||
schema: { type: 'number' },
|
|
||||||
defaultValue: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stepsSelectable: {
|
stepsSelectable: {
|
||||||
type: 'toggle',
|
type: 'toggle',
|
||||||
displayName: 'Steps selectable',
|
displayName: 'Steps selectable',
|
||||||
|
|
@ -30,7 +43,38 @@ export const stepsConfig = {
|
||||||
schema: { type: 'boolean' },
|
schema: { type: 'boolean' },
|
||||||
defaultValue: false,
|
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: {
|
defaultSize: {
|
||||||
width: 22,
|
width: 22,
|
||||||
|
|
@ -40,46 +84,126 @@ export const stepsConfig = {
|
||||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
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: {
|
events: {
|
||||||
onSelect: { displayName: 'On select' },
|
onSelect: { displayName: 'On select' },
|
||||||
},
|
},
|
||||||
styles: {
|
styles: {
|
||||||
color: {
|
incompletedAccent: {
|
||||||
type: 'colorSwatches',
|
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: {
|
validation: {
|
||||||
schema: { type: 'string' },
|
schema: { type: 'string' },
|
||||||
defaultValue: 'var(--primary-brand)',
|
defaultValue: 'var(--primary-brand)',
|
||||||
},
|
},
|
||||||
|
accordian: 'steps',
|
||||||
},
|
},
|
||||||
textColor: {
|
completedLabel: {
|
||||||
type: 'colorSwatches',
|
type: 'colorSwatches',
|
||||||
displayName: 'Text color',
|
displayName: 'Completed label',
|
||||||
validation: {
|
validation: {
|
||||||
schema: { type: 'string' },
|
schema: { type: 'string' },
|
||||||
defaultValue: '#000000',
|
defaultValue: '#1B1F24',
|
||||||
},
|
},
|
||||||
|
accordian: 'steps',
|
||||||
},
|
},
|
||||||
theme: {
|
currentStepLabel: {
|
||||||
type: 'select',
|
type: 'colorSwatches',
|
||||||
displayName: 'Theme',
|
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: [
|
options: [
|
||||||
{ name: 'titles', value: 'titles' },
|
{ displayName: 'Default', value: 'default' },
|
||||||
{ name: 'numbers', value: 'numbers' },
|
{ displayName: 'None', value: 'none' },
|
||||||
{ name: 'plain', value: 'plain' },
|
|
||||||
],
|
],
|
||||||
validation: {
|
accordian: 'container',
|
||||||
schema: { type: 'string' },
|
|
||||||
defaultValue: 'titles',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
visibility: {
|
|
||||||
type: 'toggle',
|
|
||||||
displayName: 'Visibility',
|
|
||||||
validation: {
|
|
||||||
schema: { type: 'boolean' },
|
|
||||||
defaultValue: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
exposedVariables: {
|
exposedVariables: {
|
||||||
|
|
@ -92,17 +216,35 @@ export const stepsConfig = {
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
steps: {
|
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}}' },
|
currentStep: { value: '{{3}}' },
|
||||||
stepsSelectable: { value: true },
|
stepsSelectable: { value: true },
|
||||||
|
advanced: { value: `{{false}}` },
|
||||||
|
visibility: { value: '{{true}}' },
|
||||||
},
|
},
|
||||||
events: [],
|
events: [],
|
||||||
styles: {
|
styles: {
|
||||||
visibility: { value: '{{true}}' },
|
visibility: { value: '{{true}}' },
|
||||||
theme: { value: 'titles' },
|
// color: { value: '' },
|
||||||
color: { value: 'var(--primary-brand)' },
|
// textColor: { value: '' },
|
||||||
textColor: { value: '' },
|
padding: { value: 'default' },
|
||||||
|
incompletedAccent: { value: '#E4E7EB' },
|
||||||
|
incompletedLabel: { value: '#1B1F24' },
|
||||||
|
completedAccent: { value: 'var(--primary-brand)' },
|
||||||
|
completedLabel: { value: '#1B1F24' },
|
||||||
|
currentStepLabel: { value: '#1B1F24' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,11 @@ export const createDataQuerySlice = (set, get) => ({
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.dataQuery.creatingQueryInProcessId = null;
|
state.dataQuery.creatingQueryInProcessId = null;
|
||||||
state.dataQuery.queries.modules[moduleId] = [
|
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],
|
...state.dataQuery.queries.modules[moduleId],
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,36 @@
|
||||||
export const ActionTypes = [
|
export const ActionTypes = [
|
||||||
|
{
|
||||||
|
name: 'Run query',
|
||||||
|
id: 'run-query',
|
||||||
|
options: [{ queryId: '' }],
|
||||||
|
group: 'run-action',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Show Alert',
|
name: 'Show Alert',
|
||||||
id: 'show-alert',
|
id: 'show-alert',
|
||||||
options: [{ name: 'message', type: 'text', default: 'Message !' }],
|
options: [{ name: 'message', type: 'text', default: 'Message !' }],
|
||||||
|
group: 'run-action',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Logout',
|
name: 'Control component',
|
||||||
id: 'logout',
|
id: 'control-component',
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
|
||||||
options: [
|
options: [
|
||||||
{ name: 'app', type: 'text', default: '' },
|
{ name: 'component', type: 'text', default: '' },
|
||||||
{ name: 'queryParams', type: 'code', default: '[]' },
|
{ name: 'action', type: 'text', default: '' },
|
||||||
],
|
],
|
||||||
|
group: 'control-component',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Show Modal',
|
name: 'Show modal',
|
||||||
id: 'show-modal',
|
id: 'show-modal',
|
||||||
options: [{ name: 'modal', type: 'text', default: '' }],
|
options: [{ name: 'modal', type: 'text', default: '' }],
|
||||||
|
group: 'control-component',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Close Modal',
|
name: 'Close modal',
|
||||||
id: 'close-modal',
|
id: 'close-modal',
|
||||||
options: [{ name: 'modal', type: 'text', default: '' }],
|
options: [{ name: 'modal', type: 'text', default: '' }],
|
||||||
},
|
group: 'control-component',
|
||||||
{
|
|
||||||
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: '{{[]}}' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Set table page',
|
name: 'Set table page',
|
||||||
|
|
@ -69,28 +43,28 @@ export const ActionTypes = [
|
||||||
},
|
},
|
||||||
{ name: 'pageIndex', type: 'text', default: '{{1}}' },
|
{ name: 'pageIndex', type: 'text', default: '{{1}}' },
|
||||||
],
|
],
|
||||||
},
|
group: 'control-component',
|
||||||
{
|
|
||||||
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: '' }],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Switch page',
|
name: 'Switch page',
|
||||||
id: 'switch-page',
|
id: 'switch-page',
|
||||||
options: [{ name: 'page', type: 'text', default: '' }],
|
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',
|
name: 'Set page variable',
|
||||||
|
|
@ -99,10 +73,7 @@ export const ActionTypes = [
|
||||||
{ name: 'key', type: 'code', default: '' },
|
{ name: 'key', type: 'code', default: '' },
|
||||||
{ name: 'value', type: 'code', default: '' },
|
{ name: 'value', type: 'code', default: '' },
|
||||||
],
|
],
|
||||||
},
|
group: 'variable',
|
||||||
{
|
|
||||||
name: 'Unset all page variables',
|
|
||||||
id: 'unset-all-page-variables',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Unset page variable',
|
name: 'Unset page variable',
|
||||||
|
|
@ -111,14 +82,61 @@ export const ActionTypes = [
|
||||||
{ name: 'key', type: 'code', default: '' },
|
{ name: 'key', type: 'code', default: '' },
|
||||||
{ name: 'value', type: 'code', default: '' },
|
{ name: 'value', type: 'code', default: '' },
|
||||||
],
|
],
|
||||||
|
group: 'variable',
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'Control component',
|
name: 'Unset all page variables',
|
||||||
id: 'control-component',
|
id: 'unset-all-page-variables',
|
||||||
|
group: 'variable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Set variable',
|
||||||
|
id: 'set-custom-variable',
|
||||||
options: [
|
options: [
|
||||||
{ name: 'component', type: 'text', default: '' },
|
{ name: 'key', type: 'code', default: '' },
|
||||||
{ name: 'action', type: 'text', default: '' },
|
{ name: 'value', type: 'code', default: '' },
|
||||||
],
|
],
|
||||||
|
group: 'variable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Unset variable',
|
||||||
|
id: 'unset-custom-variable',
|
||||||
|
options: [{ name: 'key', type: 'code', default: '' }],
|
||||||
|
group: 'variable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Unset all variables',
|
||||||
|
id: 'unset-all-custom-variables',
|
||||||
|
group: 'variable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Logout',
|
||||||
|
id: 'logout',
|
||||||
|
group: 'other',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Generate file',
|
||||||
|
id: 'generate-file',
|
||||||
|
options: [
|
||||||
|
{ name: 'fileType', type: 'text', default: '' },
|
||||||
|
{ name: 'fileName', type: 'text', default: '' },
|
||||||
|
{ name: 'data', type: 'code', default: '{{[]}}' },
|
||||||
|
],
|
||||||
|
group: 'other',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Set local storage',
|
||||||
|
id: 'set-localstorage-value',
|
||||||
|
options: [
|
||||||
|
{ name: 'key', type: 'code', default: '' },
|
||||||
|
{ name: 'value', type: 'code', default: '' },
|
||||||
|
],
|
||||||
|
group: 'other',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Copy to clipboard',
|
||||||
|
id: 'copy-to-clipboard',
|
||||||
|
options: [{ name: 'copy-to-clipboard', type: 'text', default: '' }],
|
||||||
|
group: 'other',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
/* eslint-disable react/no-string-refs */
|
/* eslint-disable react/no-string-refs */
|
||||||
import React from 'react';
|
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 'draft-js/dist/Draft.css';
|
||||||
|
import { stateFromHTML } from 'draft-js-import-html';
|
||||||
import { stateToHTML } from 'draft-js-export-html';
|
import { stateToHTML } from 'draft-js-export-html';
|
||||||
import Loader from '@/ToolJetUI/Loader/Loader';
|
import Loader from '@/ToolJetUI/Loader/Loader';
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
|
|
@ -150,11 +151,8 @@ const InlineStyleControls = (props) => {
|
||||||
class DraftEditor extends React.Component {
|
class DraftEditor extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(this.props.defaultValue));
|
|
||||||
this.state = {
|
this.state = {
|
||||||
editorState: EditorState.createWithContent(
|
editorState: EditorState.createWithContent(stateFromHTML(DOMPurify.sanitize(this.props.defaultValue))),
|
||||||
ContentState.createFromBlockArray(blocksFromHTML.contentBlocks, blocksFromHTML.entityMap)
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.editorContainerRef = React.createRef();
|
this.editorContainerRef = React.createRef();
|
||||||
|
|
@ -173,6 +171,18 @@ class DraftEditor extends React.Component {
|
||||||
this.toggleInlineStyle = this._toggleInlineStyle.bind(this);
|
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() {
|
componentDidMount() {
|
||||||
//For resizing the editor container based on the height of rich text editor controls
|
//For resizing the editor container based on the height of rich text editor controls
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
|
@ -193,11 +203,7 @@ class DraftEditor extends React.Component {
|
||||||
isVisible: this.props.isVisible,
|
isVisible: this.props.isVisible,
|
||||||
isLoading: this.props.isLoading,
|
isLoading: this.props.isLoading,
|
||||||
setValue: async (text) => {
|
setValue: async (text) => {
|
||||||
const blocksFromHTML = convertFromHTML(DOMPurify.sanitize(text));
|
const newContentState = stateFromHTML(DOMPurify.sanitize(text));
|
||||||
const newContentState = ContentState.createFromBlockArray(
|
|
||||||
blocksFromHTML.contentBlocks,
|
|
||||||
blocksFromHTML.entityMap
|
|
||||||
);
|
|
||||||
const newEditorState = EditorState.createWithContent(newContentState);
|
const newEditorState = EditorState.createWithContent(newContentState);
|
||||||
const html = stateToHTML(newContentState);
|
const html = stateToHTML(newContentState);
|
||||||
this.props.handleChange(html);
|
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) {
|
_handleKeyCommand(command, editorState) {
|
||||||
const newState = RichUtils.handleKeyCommand(editorState, command);
|
const newState = RichUtils.handleKeyCommand(editorState, command);
|
||||||
if (newState) {
|
if (newState) {
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,226 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { isExpectedDataType } from '@/_helpers/utils';
|
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 }) {
|
export const Steps = function Steps({ properties, styles, fireEvent, setExposedVariable, height, darkMode, dataCy }) {
|
||||||
const { stepsSelectable } = properties;
|
const { stepsSelectable, disabledState } = properties;
|
||||||
const currentStep = isExpectedDataType(properties.currentStep, 'number');
|
const visibility = isExpectedDataType(properties.visibility, 'boolean');
|
||||||
const steps = isExpectedDataType(properties.steps, 'array');
|
const currentStepId = isExpectedDataType(properties.currentStep, 'number');
|
||||||
const { color, theme, visibility, boxShadow } = styles;
|
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 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 = {
|
const dynamicStyle = {
|
||||||
'--bgColor': styles.color,
|
'--bgColor': styles.color,
|
||||||
'--textColor': textColor,
|
'--textColor': textColor,
|
||||||
};
|
'--completedAccent': completedAccent === '#4368E3' ? 'var(--primary-brand)' : completedAccent,
|
||||||
const activeStepHandler = (id) => {
|
'--incompletedAccent': incompletedAccent === '#E4E7EB' ? 'var(--surfaces-surface-03)' : incompletedAccent,
|
||||||
const active = steps.filter((item) => item.id == id);
|
'--incompletedLabel': incompletedLabel === '#1B1F24' ? 'var(--text-primary)' : incompletedLabel,
|
||||||
setExposedVariable('currentStepId', active[0].id);
|
'--completedLabel': completedLabel === '#1B1F24' ? 'var(--text-primary)' : completedLabel,
|
||||||
fireEvent('onSelect');
|
'--currentStepLabel': currentStepLabel === '#1B1F24' ? 'var(--text-primary)' : currentStepLabel,
|
||||||
setActiveStep(active[0].id);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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(() => {
|
useEffect(() => {
|
||||||
setActiveStep(currentStep);
|
setExposedVariable('isVisible', isVisible);
|
||||||
setExposedVariable('currentStepId', currentStep);
|
setExposedVariable('isDisabled', isDisabled);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
setExposedVariable('currentStepId', activeStepId);
|
||||||
}, [currentStep]);
|
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 (
|
return (
|
||||||
visibility && (
|
<div
|
||||||
<div
|
ref={containerRef}
|
||||||
className={`steps ${theme == 'numbers' && 'steps-counter '}`}
|
className={`steps-container ${isDisabled ? 'disabled' : ''} ${filteredSteps.length === 1 ? 'single-step' : ''}`}
|
||||||
style={{ color: textColor, height, boxShadow }}
|
style={{
|
||||||
data-cy={dataCy}
|
height,
|
||||||
>
|
boxShadow,
|
||||||
{steps?.map((item) => (
|
padding: theme === 'titles' ? `0 ${containerPadding}px` : 2,
|
||||||
<a
|
paddingTop: theme === 'plain' ? `3px` : theme === 'numbers' ? `2px` : 0,
|
||||||
key={item.id}
|
...dynamicStyle
|
||||||
className={`step-item ${item.id == activeStep && 'active'} ${!stepsSelectable && 'step-item-disabled'} ${
|
}}
|
||||||
color && `step-${color}`
|
data-cy={dataCy}
|
||||||
}`}
|
>
|
||||||
data-bs-toggle="tooltip"
|
<div className={`progress-line-container ${filteredSteps.length === 1 ? 'single-step' : ''}`}>
|
||||||
title={item?.tooltip}
|
{filteredSteps.map((step, index) => {
|
||||||
onClick={() => stepsSelectable && activeStepHandler(item.id)}
|
const isStepDisabled = step.disabled;
|
||||||
style={dynamicStyle}
|
const isCompleted = index < currentStepIndex;
|
||||||
>
|
const isActive = index === currentStepIndex;
|
||||||
{theme == 'titles' && item.name}
|
const isUpcoming = index > currentStepIndex;
|
||||||
</a>
|
const isFirstStep = index === 0;
|
||||||
))}
|
const isLastStep = index === filteredSteps.length - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}> {/* using index as key to avoid issues due to duplicate step ids */}
|
||||||
|
<ToolTip
|
||||||
|
show={!step.disabled && !isDisabled && step.tooltip}
|
||||||
|
message={step.tooltip || ''}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => stepsSelectable && handleStepClick(step.id)}
|
||||||
|
className={`milestone ${theme === 'numbers' ? 'numbers' : ''} ${isDisabled || isStepDisabled ? 'disabled' : ''
|
||||||
|
} ${isCompleted ? 'completed' : isActive ? 'active' : 'incomplete'}`}
|
||||||
|
>
|
||||||
|
{theme === 'numbers' ? (
|
||||||
|
index + 1
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`dot ${isCompleted ? 'completed' : isActive ? 'active' : 'incomplete'}`}
|
||||||
|
style={{
|
||||||
|
border: `2px solid ${isCompleted ? completedAccent : isActive ? completedAccent : incompletedAccent}`,
|
||||||
|
backgroundColor: isActive ? 'transparent' : (isCompleted ? completedAccent : incompletedAccent)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{theme === 'titles' && (
|
||||||
|
<div
|
||||||
|
ref={isFirstStep ? firstLabelRef : isLastStep ? lastLabelRef : null}
|
||||||
|
className={`label ${isCompleted ? 'completed' : isActive ? 'active' : 'incomplete'}`}
|
||||||
|
style={{ maxWidth: `${progressBarWidth}px` }}
|
||||||
|
>
|
||||||
|
{step.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ToolTip>
|
||||||
|
|
||||||
|
{index < filteredSteps.length - 1 && (
|
||||||
|
<div
|
||||||
|
className={`step-connector ${isCompleted ? 'completed' : 'incomplete'}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
132
frontend/src/Editor/Components/Steps.scss
Normal file
132
frontend/src/Editor/Components/Steps.scss
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
.steps-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.single-step {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-line-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
&.single-step {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.milestone {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.numbers {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
&.completed {
|
||||||
|
background-color: var(--completedAccent);
|
||||||
|
color: var(--completedLabel);
|
||||||
|
border: 2px solid var(--completedAccent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--currentStepLabel);
|
||||||
|
border: 2px solid var(--completedAccent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.incomplete {
|
||||||
|
background-color: var(--incompletedAccent);
|
||||||
|
color: var(--incompletedLabel);
|
||||||
|
border: 2px solid var(--incompletedAccent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
&.completed {
|
||||||
|
background-color: var(--completedAccent);
|
||||||
|
border: 2px solid var(--completedAccent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: white;
|
||||||
|
border: 2px solid var(--primary-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.incomplete {
|
||||||
|
background-color: var(--incompletedAccent);
|
||||||
|
border: 2px solid var(--incompletedAccent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2px;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: max-content;
|
||||||
|
|
||||||
|
&.completed {
|
||||||
|
color: var(--completedLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--completedLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.incomplete {
|
||||||
|
color: var(--incompletedLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-connector {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 2px;
|
||||||
|
align-self: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.completed {
|
||||||
|
background-color: var(--completedAccent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.incomplete {
|
||||||
|
background-color: var(--incompletedAccent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,7 +47,7 @@ import { verticalDividerConfig } from './verticalDivider';
|
||||||
import { customComponentConfig } from './customComponent';
|
import { customComponentConfig } from './customComponent';
|
||||||
import { buttonGroupConfig } from './buttonGroup';
|
import { buttonGroupConfig } from './buttonGroup';
|
||||||
import { pdfConfig } from './pdf';
|
import { pdfConfig } from './pdf';
|
||||||
import { stepsConfig } from './steps';
|
// import { stepsConfig } from './steps';
|
||||||
import { kanbanConfig } from './kanban';
|
import { kanbanConfig } from './kanban';
|
||||||
import { colorPickerConfig } from './colorPicker';
|
import { colorPickerConfig } from './colorPicker';
|
||||||
import { treeSelectConfig } from './treeSelect';
|
import { treeSelectConfig } from './treeSelect';
|
||||||
|
|
@ -106,7 +106,7 @@ export {
|
||||||
customComponentConfig,
|
customComponentConfig,
|
||||||
buttonGroupConfig,
|
buttonGroupConfig,
|
||||||
pdfConfig,
|
pdfConfig,
|
||||||
stepsConfig,
|
// stepsConfig,
|
||||||
kanbanConfig,
|
kanbanConfig,
|
||||||
kanbanBoardConfig, //!Depreciated
|
kanbanBoardConfig, //!Depreciated
|
||||||
colorPickerConfig,
|
colorPickerConfig,
|
||||||
|
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
export const stepsConfig = {
|
|
||||||
name: 'Steps',
|
|
||||||
displayName: 'Steps',
|
|
||||||
description: 'Step-by-step navigation aid',
|
|
||||||
component: 'Steps',
|
|
||||||
properties: {
|
|
||||||
steps: {
|
|
||||||
type: 'code',
|
|
||||||
displayName: 'Steps',
|
|
||||||
validation: {
|
|
||||||
schema: {
|
|
||||||
type: 'array',
|
|
||||||
element: { type: 'object', object: { id: { type: 'number' } } },
|
|
||||||
},
|
|
||||||
defaultValue: `[{ name: 'step 1'}, {name: 'step 2'}]`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentStep: {
|
|
||||||
type: 'code',
|
|
||||||
displayName: 'Current step',
|
|
||||||
validation: {
|
|
||||||
schema: { type: 'number' },
|
|
||||||
defaultValue: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stepsSelectable: {
|
|
||||||
type: 'toggle',
|
|
||||||
displayName: 'Steps selectable',
|
|
||||||
validation: {
|
|
||||||
schema: { type: 'boolean' },
|
|
||||||
defaultValue: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultSize: {
|
|
||||||
width: 22,
|
|
||||||
height: 38,
|
|
||||||
},
|
|
||||||
others: {
|
|
||||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
|
||||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
onSelect: { displayName: 'On select' },
|
|
||||||
},
|
|
||||||
styles: {
|
|
||||||
color: {
|
|
||||||
type: 'color',
|
|
||||||
displayName: 'Color',
|
|
||||||
validation: {
|
|
||||||
schema: { type: 'string' },
|
|
||||||
defaultValue: '#000000',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
textColor: {
|
|
||||||
type: 'color',
|
|
||||||
displayName: 'Text color',
|
|
||||||
validation: {
|
|
||||||
schema: { type: 'string' },
|
|
||||||
defaultValue: '#000000',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
theme: {
|
|
||||||
type: 'select',
|
|
||||||
displayName: 'Theme',
|
|
||||||
options: [
|
|
||||||
{ name: 'titles', value: 'titles' },
|
|
||||||
{ name: 'numbers', value: 'numbers' },
|
|
||||||
{ name: 'plain', value: 'plain' },
|
|
||||||
],
|
|
||||||
validation: {
|
|
||||||
schema: { type: 'string' },
|
|
||||||
defaultValue: 'titles',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
visibility: {
|
|
||||||
type: 'toggle',
|
|
||||||
displayName: 'Visibility',
|
|
||||||
validation: {
|
|
||||||
schema: { type: 'boolean' },
|
|
||||||
defaultValue: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
exposedVariables: {
|
|
||||||
currentStepId: '3',
|
|
||||||
},
|
|
||||||
definition: {
|
|
||||||
others: {
|
|
||||||
showOnDesktop: { value: '{{true}}' },
|
|
||||||
showOnMobile: { value: '{{false}}' },
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
steps: {
|
|
||||||
value: `{{ [{ name: 'step 1', tooltip: 'some tooltip', id: 1},{ name: 'step 2', tooltip: 'some tooltip', id: 2},{ name: 'step 3', tooltip: 'some tooltip', id: 3},{ name: 'step 4', tooltip: 'some tooltip', id: 4},{ name: 'step 5', tooltip: 'some tooltip', id: 5}]}}`,
|
|
||||||
},
|
|
||||||
currentStep: { value: '{{3}}' },
|
|
||||||
stepsSelectable: { value: true },
|
|
||||||
},
|
|
||||||
events: [],
|
|
||||||
styles: {
|
|
||||||
visibility: { value: '{{true}}' },
|
|
||||||
theme: { value: 'titles' },
|
|
||||||
color: { value: '' },
|
|
||||||
textColor: { value: '' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -7675,29 +7675,33 @@ fieldset:disabled .btn {
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded {
|
.rounded {
|
||||||
border-radius: 4px ;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-0 {
|
.rounded-0 {
|
||||||
border-radius: 0 !important
|
border-radius: 0 !important
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-top-left{
|
.rounded-top-left {
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-top-left-0{
|
.rounded-top-left-0 {
|
||||||
border-top-left-radius: 0 !important;
|
border-top-left-radius: 0 !important;
|
||||||
}
|
}
|
||||||
.rounded-top-right-0{
|
|
||||||
|
.rounded-top-right-0 {
|
||||||
border-top-right-radius: 0 !important;
|
border-top-right-radius: 0 !important;
|
||||||
}
|
}
|
||||||
.rounded-bottom-left-0{
|
|
||||||
|
.rounded-bottom-left-0 {
|
||||||
border-bottom-left-radius: 0 !important;
|
border-bottom-left-radius: 0 !important;
|
||||||
}
|
}
|
||||||
.rounded-bottom-right-0{
|
|
||||||
|
.rounded-bottom-right-0 {
|
||||||
border-bottom-right-radius: 0 !important;
|
border-bottom-right-radius: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-1 {
|
.rounded-1 {
|
||||||
border-radius: 2px !important
|
border-radius: 2px !important
|
||||||
}
|
}
|
||||||
|
|
@ -17484,8 +17488,8 @@ a.step-item:hover {
|
||||||
|
|
||||||
.step-item:not(:first-child):after {
|
.step-item:not(:first-child):after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -50%;
|
left: calc(-50% + 8px);
|
||||||
width: 100%;
|
width: calc(100% - 16px);
|
||||||
content: "";
|
content: "";
|
||||||
transform: translateY(-50%)
|
transform: translateY(-50%)
|
||||||
}
|
}
|
||||||
|
|
@ -17498,13 +17502,25 @@ a.step-item:hover {
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
display: block;
|
display: block;
|
||||||
content: "";
|
content: "";
|
||||||
border: 2px solid #fff;
|
border: 2px solid transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transform: translateX(-50%)
|
transform: translateX(-50%)
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-item.active {
|
.steps.steps-counter {
|
||||||
font-weight: 600
|
.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 {
|
.step-item.active:before {
|
||||||
|
|
@ -17521,7 +17537,7 @@ a.step-item:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-item.active~.step-item:before {
|
.step-item.active~.step-item:before {
|
||||||
color: #656d77 !important
|
color: var(--incompletedLabel) !important
|
||||||
}
|
}
|
||||||
|
|
||||||
.steps-counter {
|
.steps-counter {
|
||||||
|
|
@ -17549,7 +17565,8 @@ a.step-item:hover {
|
||||||
.steps-counter .step-item:before {
|
.steps-counter .step-item:before {
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
content: counter(steps)
|
content: counter(steps);
|
||||||
|
font-weight: 500 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.steps-counter .step-item.active~.step-item:before {
|
.steps-counter .step-item.active~.step-item:before {
|
||||||
|
|
@ -19156,4 +19173,4 @@ img {
|
||||||
background: #1f2936;
|
background: #1f2936;
|
||||||
border-color: #dadcde
|
border-color: #dadcde
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4760,15 +4760,18 @@ input[type="text"] {
|
||||||
.folder-list {
|
.folder-list {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #888 transparent;
|
scrollbar-color: #888 transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
display: block;
|
display: block;
|
||||||
width: 5px;
|
width: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background-color: #888;
|
background-color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
@ -6518,6 +6521,7 @@ div#driver-page-overlay {
|
||||||
// steps-widget
|
// steps-widget
|
||||||
a.step-item-disabled {
|
a.step-item-disabled {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.steps {
|
.steps {
|
||||||
|
|
@ -6527,34 +6531,45 @@ a.step-item-disabled {
|
||||||
|
|
||||||
.step-item.active~.step-item:after,
|
.step-item.active~.step-item:after,
|
||||||
.step-item.active~.step-item:before {
|
.step-item.active~.step-item:before {
|
||||||
background: #f3f5f5 !important;
|
background: var(--incompletedAccent) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-item.active:before {
|
.step-item.active:before {
|
||||||
background: #ffffff !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.steps .step-item.active:before {
|
.steps .step-item.active:before {
|
||||||
border-color: #b4b2b2 !important;
|
border-color: var(--completedAccent) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.steps-item {
|
.steps-item {
|
||||||
color: var(--textColor) !important;
|
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 {
|
.step-item:before {
|
||||||
background: var(--bgColor) !important;
|
background-color: var(--completedAccent) !important;
|
||||||
// remaining code
|
// remaining code
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-item:after {
|
.step-item:after {
|
||||||
background: var(--bgColor) !important;
|
background: var(--completedAccent) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step-item.active~.step-item {
|
|
||||||
color: var(--textColor) !important;
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification-center-badge {
|
.notification-center-badge {
|
||||||
|
|
||||||
|
|
@ -9872,25 +9887,30 @@ tbody {
|
||||||
.workspace-settings-table-wrap {
|
.workspace-settings-table-wrap {
|
||||||
max-width: 880px;
|
max-width: 880px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
.tj-user-table-wrapper{
|
|
||||||
|
.tj-user-table-wrapper {
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
&:hover{
|
|
||||||
.tj-user-table-wrapper{
|
&:hover {
|
||||||
padding-right: 0px;
|
.tj-user-table-wrapper {
|
||||||
}
|
padding-right: 0px;
|
||||||
::-webkit-scrollbar{
|
}
|
||||||
display: block;
|
|
||||||
width: 4px;
|
::-webkit-scrollbar {
|
||||||
}
|
display: block;
|
||||||
::-webkit-scrollbar-track{
|
width: 4px;
|
||||||
background: var(--base);
|
}
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb{
|
::-webkit-scrollbar-track {
|
||||||
background: var(--slate7);
|
background: var(--base);
|
||||||
border-radius: 6px;
|
}
|
||||||
}
|
|
||||||
}
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--slate7);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12056,8 +12076,10 @@ tbody {
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-list-wrap.sidebar-list-wrap-with-banner.isAdmin {
|
.sidebar-list-wrap.sidebar-list-wrap-with-banner.isAdmin {
|
||||||
height: calc(100vh - 371px);
|
height: calc(100vh - 371px);
|
||||||
|
|
||||||
&.resource-limit-reached {
|
&.resource-limit-reached {
|
||||||
height: calc(100vh - 371px);
|
height: calc(100vh - 371px);
|
||||||
}
|
}
|
||||||
|
|
@ -15801,6 +15823,7 @@ textarea.tj-text-input-widget{
|
||||||
|
|
||||||
.rest-api-options-codehinter {
|
.rest-api-options-codehinter {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
.cm-content>.cm-line {
|
.cm-content>.cm-line {
|
||||||
// max-width: 357px !important;
|
// max-width: 357px !important;
|
||||||
}
|
}
|
||||||
|
|
@ -16232,19 +16255,20 @@ fieldset:disabled {
|
||||||
}
|
}
|
||||||
|
|
||||||
.datepicker-validation-half {
|
.datepicker-validation-half {
|
||||||
flex:1 1 calc(50% - 8px);
|
flex: 1 1 calc(50% - 8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.date-validation-wrapper {
|
.date-validation-wrapper {
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
height:24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-flex-wrapper {
|
.code-flex-wrapper {
|
||||||
flex-wrap:wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
margin-bottom: 3px;
|
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;
|
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 {
|
.datepicker-component {
|
||||||
.react-datepicker {
|
.react-datepicker {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 8px 8px 16px 0px #3032331A;
|
box-shadow: 8px 8px 16px 0px #3032331A;
|
||||||
height:auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.react-datepicker-time-component {
|
.react-datepicker-time-component {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
width:auto;
|
width: auto;
|
||||||
|
|
||||||
.custom-time-input{
|
.custom-time-input {
|
||||||
border-left:none;
|
border-left: none;
|
||||||
border-radius:10px;
|
border-radius: 10px;
|
||||||
box-shadow: 8px 8px 16px 0px #3032331A;
|
box-shadow: 8px 8px 16px 0px #3032331A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-input-body {
|
.time-input-body {
|
||||||
padding-bottom:0px;
|
padding-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-col {
|
.time-col {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
|
@ -16312,28 +16339,32 @@ fieldset:disabled {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 8px 8px 16px 0px #3032331A;
|
box-shadow: 8px 8px 16px 0px #3032331A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker-time__input-container{
|
.react-datepicker-time__input-container {
|
||||||
border-radius:10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dark-theme {
|
.dark-theme {
|
||||||
.react-datepicker__year-text, .react-datepicker__month-text {
|
|
||||||
|
.react-datepicker__year-text,
|
||||||
|
.react-datepicker__month-text {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__year-text:hover, .react-datepicker__month-text:hover {
|
.react-datepicker__year-text:hover,
|
||||||
background-color: #9ba1a6 ;
|
.react-datepicker__month-text:hover {
|
||||||
|
background-color: #9ba1a6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tj-datepicker-widget-year-selector:hover, .tj-datepicker-widget-month-selector:hover {
|
.tj-datepicker-widget-year-selector:hover,
|
||||||
padding:1px 6px;
|
.tj-datepicker-widget-month-selector:hover {
|
||||||
|
padding: 1px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker{
|
.react-datepicker {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
border-top-right-radius: 0rem;
|
border-top-right-radius: 0rem;
|
||||||
|
|
@ -16346,48 +16377,49 @@ fieldset:disabled {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__year-wrapper {
|
.react-datepicker__year-wrapper {
|
||||||
display:grid;
|
display: grid;
|
||||||
grid-template-columns:repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
max-width: unset;
|
max-width: unset;
|
||||||
gap:10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker {
|
.react-datepicker {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__header--custom{
|
.react-datepicker__header--custom {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__year--container{
|
.react-datepicker__year--container {
|
||||||
height:208px;
|
height: 208px;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
box-shadow: 8px 8px 16px 0px #3032331A;
|
box-shadow: 8px 8px 16px 0px #3032331A;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__year-text--selected {
|
.react-datepicker__year-text--selected {
|
||||||
background-color: #4368E3 !important;
|
background-color: #4368E3 !important;
|
||||||
height:24px;
|
height: 24px;
|
||||||
width:61.33px;
|
width: 61.33px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #fff ;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__year-text{
|
.react-datepicker__year-text {
|
||||||
font-family:'IBM Plex Sans' ;
|
font-family: 'IBM Plex Sans';
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
height:24px;
|
height: 24px;
|
||||||
width:61.33px;
|
width: 61.33px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display:flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16402,42 +16434,42 @@ fieldset:disabled {
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__month-container {
|
.react-datepicker__month-container {
|
||||||
height:208px;
|
height: 208px;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
box-shadow: 8px 8px 16px 0px #3032331A;
|
box-shadow: 8px 8px 16px 0px #3032331A;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__monthPicker {
|
.react-datepicker__monthPicker {
|
||||||
display:flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap:10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__month-text--selected {
|
.react-datepicker__month-text--selected {
|
||||||
background-color: #4368E3 !important;
|
background-color: #4368E3 !important;
|
||||||
height:24px;
|
height: 24px;
|
||||||
width:61.33px;
|
width: 61.33px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #fff ;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__month-wrapper {
|
.react-datepicker__month-wrapper {
|
||||||
display:flex;
|
display: flex;
|
||||||
gap:24px;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__month-text {
|
.react-datepicker__month-text {
|
||||||
font-family:'IBM Plex Sans' ;
|
font-family: 'IBM Plex Sans';
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
height:24px;
|
height: 24px;
|
||||||
width:61.33px;
|
width: 61.33px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display:flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16447,7 +16479,7 @@ fieldset:disabled {
|
||||||
|
|
||||||
.react-datepicker__month-container {
|
.react-datepicker__month-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
width:250px;
|
width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__input-time-container {
|
.react-datepicker__input-time-container {
|
||||||
|
|
@ -16462,12 +16494,12 @@ fieldset:disabled {
|
||||||
color: #ccc !important;
|
color: #ccc !important;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker-time__input {
|
.react-datepicker-time__input {
|
||||||
margin-left: 0px !important;
|
margin-left: 0px !important;
|
||||||
|
|
||||||
.dark-time-input {
|
.dark-time-input {
|
||||||
color:#f4f6fa !important;
|
color: #f4f6fa !important;
|
||||||
background-color: var(--surfaces-surface-01) !important;
|
background-color: var(--surfaces-surface-01) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16475,15 +16507,15 @@ fieldset:disabled {
|
||||||
.react-datepicker-wrapper {
|
.react-datepicker-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker-time__caption {
|
.react-datepicker-time__caption {
|
||||||
display:none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-time-input {
|
.custom-time-input {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-left: 1px solid #CCD1D5;
|
border-left: 1px solid #CCD1D5;
|
||||||
border-top-right-radius: 10px;
|
border-top-right-radius: 10px;
|
||||||
border-bottom-right-radius: 10px;
|
border-bottom-right-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16497,18 +16529,18 @@ fieldset:disabled {
|
||||||
border-bottom: 1px solid #CCD1D5;
|
border-bottom: 1px solid #CCD1D5;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: 'IBM Plex Sans';
|
font-family: 'IBM Plex Sans';
|
||||||
color:#ACB2B9;
|
color: #ACB2B9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-input-body {
|
.time-input-body {
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-col {
|
.time-col {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
height: 265px;
|
height: 265px;
|
||||||
width: 62px;
|
width: 62px;
|
||||||
}
|
}
|
||||||
|
|
@ -16516,12 +16548,12 @@ fieldset:disabled {
|
||||||
.selected-time {
|
.selected-time {
|
||||||
background-color: #4368E3 !important;
|
background-color: #4368E3 !important;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color:#fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-item {
|
.time-item {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height:22px;
|
height: 22px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -16861,16 +16893,17 @@ section.ai-message-prompt-input-wrapper {
|
||||||
|
|
||||||
|
|
||||||
.tj-inspector-timepicker.dark-theme {
|
.tj-inspector-timepicker.dark-theme {
|
||||||
.react-datepicker {
|
.react-datepicker {
|
||||||
color:#f4f6fa !important;
|
color: #f4f6fa !important;
|
||||||
background-color: var(--surfaces-surface-01) !important;
|
background-color: var(--surfaces-surface-01) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker, .react-datepicker__header {
|
.react-datepicker,
|
||||||
|
.react-datepicker__header {
|
||||||
border: 1px solid var(--borders-default);
|
border: 1px solid var(--borders-default);
|
||||||
background-color: #1f2936;
|
background-color: #1f2936;
|
||||||
|
|
||||||
.react-datepicker-time__header{
|
.react-datepicker-time__header {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16878,25 +16911,27 @@ section.ai-message-prompt-input-wrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
.tj-inspector-timepicker {
|
.tj-inspector-timepicker {
|
||||||
padding:0px !important;
|
padding: 0px !important;
|
||||||
|
|
||||||
.react-datepicker__time-list {
|
.react-datepicker__time-list {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__triangle {
|
.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;
|
flex-basis: 100% !important;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
height:32px;
|
height: 32px;
|
||||||
|
|
||||||
.react-datepicker-wrapper {
|
.react-datepicker-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid var(--slate7);
|
border: 1px solid var(--slate7);
|
||||||
|
|
@ -16904,23 +16939,23 @@ section.ai-message-prompt-input-wrapper {
|
||||||
background-color: var(--base);
|
background-color: var(--base);
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: rgb(0, 92, 197);
|
color: rgb(0, 92, 197);
|
||||||
height:32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.dark-theme {
|
input.dark-theme {
|
||||||
background-color: #272822;
|
background-color: #272822;
|
||||||
color: rgb(174, 129, 255);
|
color: rgb(174, 129, 255);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-inspector-validation-time-picker {
|
.custom-inspector-validation-time-picker {
|
||||||
.custom-time-input {
|
.custom-time-input {
|
||||||
border-left:none;
|
border-left: none;
|
||||||
border-radius:10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-col {
|
.time-col {
|
||||||
|
|
@ -16928,19 +16963,21 @@ section.ai-message-prompt-input-wrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__input-time-container {
|
.react-datepicker__input-time-container {
|
||||||
border-radius:10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-inspector-validation-time-picker-popper {
|
.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;
|
height: 60px;
|
||||||
|
|
||||||
.hide-fx {
|
.hide-fx {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
|
|
@ -16959,8 +16996,9 @@ section.ai-message-prompt-input-wrapper {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__day:hover, .react-datepicker__day--selecting-range-end {
|
.react-datepicker__day:hover,
|
||||||
background-color: var(--interactive-overlays-fill-hover) !important ;
|
.react-datepicker__day--selecting-range-end {
|
||||||
|
background-color: var(--interactive-overlays-fill-hover) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__day--keyboard-selected {
|
.react-datepicker__day--keyboard-selected {
|
||||||
|
|
@ -16983,15 +17021,17 @@ section.ai-message-prompt-input-wrapper {
|
||||||
|
|
||||||
.tj-daterange-widget {
|
.tj-daterange-widget {
|
||||||
|
|
||||||
border-radius:10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0px 8px 16px 0px #3032331A !important;
|
box-shadow: 0px 8px 16px 0px #3032331A !important;
|
||||||
font-family: 'IBM Plex Sans';
|
font-family: 'IBM Plex Sans';
|
||||||
|
|
||||||
.react-datepicker__day--in-selecting-range, .react-datepicker__day--in-range {
|
.react-datepicker__day--in-selecting-range,
|
||||||
border-radius:0px;
|
.react-datepicker__day--in-range {
|
||||||
|
border-radius: 0px;
|
||||||
background-color: #4368E31A !important;
|
background-color: #4368E31A !important;
|
||||||
}
|
}
|
||||||
.react-datepicker__header{
|
|
||||||
|
.react-datepicker__header {
|
||||||
background-color: var(--surfaces-surface-01);
|
background-color: var(--surfaces-surface-01);
|
||||||
padding: 6px 0px;
|
padding: 6px 0px;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -17002,44 +17042,48 @@ section.ai-message-prompt-input-wrapper {
|
||||||
background-color: #ededee !important;
|
background-color: #ededee !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__day--selecting-range-start, .react-datepicker__day--selected, .react-datepicker__day--range-end {
|
.react-datepicker__day--selecting-range-start,
|
||||||
border-radius:8px !important;
|
.react-datepicker__day--selected,
|
||||||
|
.react-datepicker__day--range-end {
|
||||||
|
border-radius: 8px !important;
|
||||||
background-color: #4368E3 !important;
|
background-color: #4368E3 !important;
|
||||||
color: #fff !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-top-right-radius: 8px;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__day--in-range:has(+ .react-datepicker__day--range-end) {
|
.react-datepicker__day--in-range:has(+ .react-datepicker__day--range-end) {
|
||||||
box-shadow: 10px 0 0 0px #4368E31A;
|
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-top-left-radius: 8px;
|
||||||
border-bottom-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;
|
box-shadow: -10px 0 0 0px #4368E31A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__week {
|
.react-datepicker__week {
|
||||||
|
|
||||||
.react-datepicker__day--in-range:first-of-type,
|
.react-datepicker__day--in-range:first-of-type,
|
||||||
.react-datepicker__day--in-selecting-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-range,
|
||||||
.react-datepicker__day--outside-month + .react-datepicker__day--in-selecting-range{
|
.react-datepicker__day--outside-month+.react-datepicker__day--in-selecting-range {
|
||||||
border-top-left-radius: 8px;
|
border-top-left-radius: 8px;
|
||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__day--in-range:last-of-type,
|
.react-datepicker__day--in-range:last-of-type,
|
||||||
.react-datepicker__day--in-selecting-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-range:has(+ .react-datepicker__day--outside-month),
|
||||||
.react-datepicker__day--in-selecting-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-top-right-radius: 8px;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
@ -17057,8 +17101,8 @@ section.ai-message-prompt-input-wrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
.tj-datepicker-widget-right {
|
.tj-datepicker-widget-right {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tj-datepicker-widget-left {
|
.tj-datepicker-widget-left {
|
||||||
|
|
@ -17081,41 +17125,42 @@ section.ai-message-prompt-input-wrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker {
|
.react-datepicker {
|
||||||
border-radius:10px !important;
|
border-radius: 10px !important;
|
||||||
border:none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tj-daterangepicker-widget-month-selector, .tj-daterangepicker-widget-year-selector {
|
.tj-daterangepicker-widget-month-selector,
|
||||||
appearance: none;
|
.tj-daterangepicker-widget-year-selector {
|
||||||
-moz-appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-moz-appearance: none;
|
||||||
padding-right: 4px;
|
-webkit-appearance: none;
|
||||||
/* Add some padding on the right to create space for custom arrow */
|
padding-right: 4px;
|
||||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23424242" width="18px" height="18px"><path d="M7 10l5 5 5-5z" /></svg>');
|
/* Add some padding on the right to create space for custom arrow */
|
||||||
/* Add a custom arrow (you can use your own SVG) */
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23424242" width="18px" height="18px"><path d="M7 10l5 5 5-5z" /></svg>');
|
||||||
background-repeat: no-repeat;
|
/* Add a custom arrow (you can use your own SVG) */
|
||||||
background-position: right center;
|
background-repeat: no-repeat;
|
||||||
border: none;
|
background-position: right center;
|
||||||
/* Remove the default border */
|
border: none;
|
||||||
padding: 8px;
|
/* Remove the default border */
|
||||||
/* Adjust padding as needed */
|
padding: 8px;
|
||||||
cursor: pointer;
|
/* Adjust padding as needed */
|
||||||
/* Add pointer cursor for better usability */
|
cursor: pointer;
|
||||||
background: none;
|
/* Add pointer cursor for better usability */
|
||||||
padding: 0px;
|
background: none;
|
||||||
height: 24px;
|
padding: 0px;
|
||||||
text-align: center;
|
height: 24px;
|
||||||
color: var(--text-primary);
|
text-align: center;
|
||||||
font-weight: 500;
|
color: var(--text-primary);
|
||||||
width:auto;
|
font-weight: 500;
|
||||||
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.datepicker-widget {
|
.datepicker-widget {
|
||||||
.react-datepicker-wrapper{
|
.react-datepicker-wrapper {
|
||||||
width:100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17129,26 +17174,29 @@ section.ai-message-prompt-input-wrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
.tj-daterange-widget.react-datepicker-month-component {
|
.tj-daterange-widget.react-datepicker-month-component {
|
||||||
border-radius:10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0px 8px 16px 0px #3032331A !important;
|
box-shadow: 0px 8px 16px 0px #3032331A !important;
|
||||||
font-family: 'IBM Plex Sans';
|
font-family: 'IBM Plex Sans';
|
||||||
|
|
||||||
.react-datepicker__month-container {
|
.react-datepicker__month-container {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.react-datepicker__month-text {
|
.react-datepicker__month-text {
|
||||||
height:26px !important;
|
height: 26px !important;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
width:100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__month-text--in-selecting-range, .react-datepicker__month-text--in-range {
|
.react-datepicker__month-text--in-selecting-range,
|
||||||
border-radius:0px;
|
.react-datepicker__month-text--in-range {
|
||||||
|
border-radius: 0px;
|
||||||
background-color: #4368E31A !important;
|
background-color: #4368E31A !important;
|
||||||
color:#000;
|
color: #000;
|
||||||
}
|
}
|
||||||
.react-datepicker__header{
|
|
||||||
|
.react-datepicker__header {
|
||||||
background-color: var(--surfaces-surface-01);
|
background-color: var(--surfaces-surface-01);
|
||||||
padding: 6px 0px;
|
padding: 6px 0px;
|
||||||
border: none;
|
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 {
|
.react-datepicker__month-text--selecting-range-start,
|
||||||
border-radius:8px !important;
|
.react-datepicker__month-text--selected,
|
||||||
|
.react-datepicker__month-text--range-end {
|
||||||
|
border-radius: 8px !important;
|
||||||
background-color: #4368E3 !important;
|
background-color: #4368E3 !important;
|
||||||
color: #fff !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-top-right-radius: 8px;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__month-text--in-range:has(+ .react-datepicker__month-text--range-end) {
|
.react-datepicker__month-text--in-range:has(+ .react-datepicker__month-text--range-end) {
|
||||||
box-shadow: 10px 0 0 0px #4368E31A;
|
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-top-left-radius: 8px;
|
||||||
border-bottom-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;
|
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-wrapper {
|
||||||
.react-datepicker__month-text--in-selecting-range:first-of-type,
|
gap: 0px !important;
|
||||||
.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-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-top-left-radius: 8px;
|
||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__month-text--in-range:last-of-type,
|
.react-datepicker__month-text--in-range:last-of-type,
|
||||||
.react-datepicker__month-text--in-selecting-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-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-selecting-range:has(+ .react-datepicker__month-text--outside-month-text) {
|
||||||
border-top-right-radius: 8px;
|
border-top-right-radius: 8px;
|
||||||
border-bottom-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 {
|
.tj-daterange-widget.react-datepicker-year-component {
|
||||||
border-radius:10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0px 8px 16px 0px #3032331A !important;
|
box-shadow: 0px 8px 16px 0px #3032331A !important;
|
||||||
font-family: 'IBM Plex Sans';
|
font-family: 'IBM Plex Sans';
|
||||||
|
|
||||||
.react-datepicker__year-container {
|
.react-datepicker__year-container {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__year-wrapper{
|
.react-datepicker__year-wrapper {
|
||||||
gap:0px !important;
|
gap: 0px !important;
|
||||||
|
|
||||||
.react-datepicker__year-text--in-range:first-of-type,
|
.react-datepicker__year-text--in-range:first-of-type,
|
||||||
.react-datepicker__year-text--in-selecting-range:first-of-type{
|
.react-datepicker__year-text--in-selecting-range:first-of-type {
|
||||||
border-top-left-radius: 8px;
|
border-top-left-radius: 8px;
|
||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__year-text--in-range:last-of-type,
|
.react-datepicker__year-text--in-range:last-of-type,
|
||||||
.react-datepicker__year-text--in-selecting-range:last-of-type{
|
.react-datepicker__year-text--in-selecting-range:last-of-type {
|
||||||
border-top-right-radius: 8px;
|
border-top-right-radius: 8px;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.react-datepicker__year-text {
|
.react-datepicker__year-text {
|
||||||
height:26px !important;
|
height: 26px !important;
|
||||||
margin-top: 5px !important;
|
margin-top: 5px !important;
|
||||||
margin-bottom:5px !important;
|
margin-bottom: 5px !important;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
width:62px !important;
|
width: 62px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__year-text--in-selecting-range, .react-datepicker__year-text--in-range {
|
.react-datepicker__year-text--in-selecting-range,
|
||||||
border-radius:0px;
|
.react-datepicker__year-text--in-range {
|
||||||
|
border-radius: 0px;
|
||||||
background-color: #4368E31A !important;
|
background-color: #4368E31A !important;
|
||||||
color:#000;
|
color: #000;
|
||||||
}
|
}
|
||||||
.react-datepicker__header{
|
|
||||||
|
.react-datepicker__header {
|
||||||
background-color: var(--surfaces-surface-01);
|
background-color: var(--surfaces-surface-01);
|
||||||
padding: 6px 0px;
|
padding: 6px 0px;
|
||||||
border: none;
|
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 {
|
.react-datepicker__year-text--selecting-range-start,
|
||||||
border-radius:8px !important;
|
.react-datepicker__year-text--selected,
|
||||||
|
.react-datepicker__year-text--range-end {
|
||||||
|
border-radius: 8px !important;
|
||||||
background-color: #4368E3 !important;
|
background-color: #4368E3 !important;
|
||||||
color: #fff !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-top-right-radius: 8px;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-datepicker__year-text--in-range:has(+ .react-datepicker__year-text--range-end) {
|
.react-datepicker__year-text--in-range:has(+ .react-datepicker__year-text--range-end) {
|
||||||
box-shadow: 10px 0 0 0px #4368E31A;
|
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-top-left-radius: 8px;
|
||||||
border-bottom-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;
|
box-shadow: -10px 0 0 0px #4368E31A;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark-theme {
|
.dark-theme {
|
||||||
|
|
@ -18783,6 +18842,7 @@ section.ai-message-prompt-input-wrapper {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
|
||||||
&.dark {
|
&.dark {
|
||||||
background: #FFFAEB !important;
|
background: #FFFAEB !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
81
server/data-migrations/1742369436314-StepsV2Migration.ts
Normal file
81
server/data-migrations/1742369436314-StepsV2Migration.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { Component } from '@entities/component.entity';
|
||||||
|
import { EntityManager, MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
import { processDataInBatches } from '@helpers/migration.helper';
|
||||||
|
|
||||||
|
export class StepsV2Migration1742369436314 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
const componentTypes = ['Steps'];
|
||||||
|
const batchSize = 100;
|
||||||
|
const entityManager = queryRunner.manager;
|
||||||
|
|
||||||
|
for (const componentType of componentTypes) {
|
||||||
|
await processDataInBatches(
|
||||||
|
entityManager,
|
||||||
|
async (entityManager: EntityManager) => {
|
||||||
|
return await entityManager.find(Component, {
|
||||||
|
where: { type: componentType },
|
||||||
|
order: { createdAt: 'ASC' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async (entityManager: EntityManager, components: Component[]) => {
|
||||||
|
await this.processUpdates(entityManager, components);
|
||||||
|
},
|
||||||
|
batchSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||||
|
|
||||||
|
private async processUpdates(entityManager, components) {
|
||||||
|
for (const component of components) {
|
||||||
|
const properties = component.properties;
|
||||||
|
const styles = component.styles;
|
||||||
|
const general = component.general;
|
||||||
|
const generalStyles = component.generalStyles;
|
||||||
|
const validation = component.validation;
|
||||||
|
|
||||||
|
if (styles.visibility) {
|
||||||
|
properties.visibility = styles.visibility;
|
||||||
|
delete styles.visibility;
|
||||||
|
}
|
||||||
|
if (styles.theme) {
|
||||||
|
properties['variant'] = styles.theme;
|
||||||
|
delete styles.theme;
|
||||||
|
}
|
||||||
|
if (styles.color) {
|
||||||
|
styles['completedAccent'] = styles.color;
|
||||||
|
}
|
||||||
|
delete styles.color;
|
||||||
|
if (styles.textColor) {
|
||||||
|
styles['completedLabel'] = styles.textColor;
|
||||||
|
styles['incompletedLabel'] = styles.textColor;
|
||||||
|
styles['currentStepLabel'] = styles.textColor;
|
||||||
|
}
|
||||||
|
delete styles.textColor;
|
||||||
|
if (properties.steps) {
|
||||||
|
properties['schema'] = properties.steps;
|
||||||
|
delete properties.steps;
|
||||||
|
properties['advanced'] = { value: '{{true}}' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (properties.stepsSelectable) {
|
||||||
|
// properties.disabledState = styles.disabledState;
|
||||||
|
// delete styles.disabledState;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (generalStyles?.boxShadow) {
|
||||||
|
// styles.boxShadow = generalStyles?.boxShadow;
|
||||||
|
// delete generalStyles?.boxShadow;
|
||||||
|
// }
|
||||||
|
|
||||||
|
await entityManager.update(Component, component.id, {
|
||||||
|
properties,
|
||||||
|
styles,
|
||||||
|
general,
|
||||||
|
generalStyles,
|
||||||
|
validation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ import Ajv from 'ajv';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { ImportResourcesDto } from '@dto/import-resources.dto';
|
import { ImportResourcesDto } from '@dto/import-resources.dto';
|
||||||
|
import { AppImportRequestDto } from '@modules/external-apis/dto';
|
||||||
|
|
||||||
const ajv = new Ajv({ allErrors: true, coerceTypes: true });
|
const ajv = new Ajv({ allErrors: true, coerceTypes: true });
|
||||||
const logger = new Logger('TooljetDatabaseSchemaValidator');
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { AppsSubscriber } from './subscribers/apps.subscriber';
|
||||||
import { AiModule } from '@modules/ai/module';
|
import { AiModule } from '@modules/ai/module';
|
||||||
import { AppPermissionsModule } from '@modules/app-permissions/module';
|
import { AppPermissionsModule } from '@modules/app-permissions/module';
|
||||||
import { RolesRepository } from '@modules/roles/repository';
|
import { RolesRepository } from '@modules/roles/repository';
|
||||||
|
import { UsersModule } from '@modules/users/module';
|
||||||
@Module({})
|
@Module({})
|
||||||
export class AppsModule {
|
export class AppsModule {
|
||||||
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||||
|
|
@ -55,6 +56,7 @@ export class AppsModule {
|
||||||
await DataSourcesModule.register(configs),
|
await DataSourcesModule.register(configs),
|
||||||
await AiModule.register(configs),
|
await AiModule.register(configs),
|
||||||
await AppPermissionsModule.register(configs),
|
await AppPermissionsModule.register(configs),
|
||||||
|
await UsersModule.register(configs),
|
||||||
],
|
],
|
||||||
controllers: [AppsController],
|
controllers: [AppsController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|
@ -74,7 +76,7 @@ export class AppsModule {
|
||||||
AppImportExportService,
|
AppImportExportService,
|
||||||
RolesRepository,
|
RolesRepository,
|
||||||
],
|
],
|
||||||
exports: [AppsUtilService],
|
exports: [AppsUtilService, AppImportExportService],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { App } from '@entities/app.entity';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource, Repository } from 'typeorm';
|
import { DataSource, Repository } from 'typeorm';
|
||||||
import { SessionAppData } from './types';
|
import { SessionAppData } from './types';
|
||||||
|
import { WorkspaceAppsResponseDto } from '@modules/external-apis/dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppsRepository extends Repository<App> {
|
export class AppsRepository extends Repository<App> {
|
||||||
|
|
@ -63,4 +64,23 @@ export class AppsRepository extends Repository<App> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findAllOrganizationApps(organizationId: string): Promise<WorkspaceAppsResponseDto[]> {
|
||||||
|
return await this.createQueryBuilder('app')
|
||||||
|
.select([
|
||||||
|
'app.id AS id',
|
||||||
|
'app.name AS name',
|
||||||
|
'app.slug AS slug',
|
||||||
|
'app.created_at AS createdAt',
|
||||||
|
'app.organization_id AS organizationId',
|
||||||
|
'version.id AS versionId',
|
||||||
|
'version.name AS versionName',
|
||||||
|
'version.created_at AS versionCreatedAt',
|
||||||
|
])
|
||||||
|
.leftJoin('app_versions', 'version', 'version.app_id = app.id')
|
||||||
|
.where('app.organizationId = :organizationId', { organizationId })
|
||||||
|
.orderBy('app.created_At', 'ASC')
|
||||||
|
.orderBy('version.created_at', 'ASC')
|
||||||
|
.getRawMany();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,6 @@ import { VersionRepository } from '@modules/versions/repository';
|
||||||
import { AppsRepository } from './repository';
|
import { AppsRepository } from './repository';
|
||||||
import { FoldersUtilService } from '@modules/folders/util.service';
|
import { FoldersUtilService } from '@modules/folders/util.service';
|
||||||
import { FolderAppsUtilService } from '@modules/folder-apps/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 { PageService } from './services/page.service';
|
||||||
import { EventsService } from './services/event.service';
|
import { EventsService } from './services/event.service';
|
||||||
import { LICENSE_FIELD } from '@modules/licensing/constants';
|
import { LICENSE_FIELD } from '@modules/licensing/constants';
|
||||||
|
|
@ -224,40 +221,7 @@ export class AppsService implements IAppsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async findTooljetDbTables(appId: string): Promise<{ table_id: string }[]> {
|
async findTooljetDbTables(appId: string): Promise<{ table_id: string }[]> {
|
||||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
return await this.appsUtilService.findTooljetDbTables(appId); //moved to util
|
||||||
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 };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOne(app: App, user: User): Promise<any> {
|
async getOne(app: App, user: User): Promise<any> {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import { DataSourcesUtilService } from '@modules/data-sources/util.service';
|
||||||
import { DataSourcesRepository } from '@modules/data-sources/repository';
|
import { DataSourcesRepository } from '@modules/data-sources/repository';
|
||||||
import { AppEnvironmentUtilService } from '@modules/app-environments/util.service';
|
import { AppEnvironmentUtilService } from '@modules/app-environments/util.service';
|
||||||
import { ComponentsService } from './component.service';
|
import { ComponentsService } from './component.service';
|
||||||
|
import { UsersUtilService } from '@modules/users/util.service';
|
||||||
interface AppResourceMappings {
|
interface AppResourceMappings {
|
||||||
defaultDataSourceIdMapping: Record<string, string>;
|
defaultDataSourceIdMapping: Record<string, string>;
|
||||||
dataQueryMapping: Record<string, string>;
|
dataQueryMapping: Record<string, string>;
|
||||||
|
|
@ -51,7 +52,17 @@ type DefaultDataSourceName =
|
||||||
| 'tooljetdbdefault'
|
| 'tooljetdbdefault'
|
||||||
| 'workflowsdefault';
|
| '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[] = [
|
const DefaultDataSourceNames: DefaultDataSourceName[] = [
|
||||||
'restapidefault',
|
'restapidefault',
|
||||||
|
|
@ -80,9 +91,10 @@ export class AppImportExportService {
|
||||||
protected dataSourcesUtilService: DataSourcesUtilService,
|
protected dataSourcesUtilService: DataSourcesUtilService,
|
||||||
protected dataSourcesRepository: DataSourcesRepository,
|
protected dataSourcesRepository: DataSourcesRepository,
|
||||||
protected appEnvironmentUtilService: AppEnvironmentUtilService,
|
protected appEnvironmentUtilService: AppEnvironmentUtilService,
|
||||||
|
protected usersUtilService: UsersUtilService,
|
||||||
protected readonly entityManager: EntityManager,
|
protected readonly entityManager: EntityManager,
|
||||||
protected componentsService: ComponentsService
|
protected componentsService: ComponentsService
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
async export(user: User, id: string, searchParams: any = {}): Promise<{ appV2: App }> {
|
async export(user: User, id: string, searchParams: any = {}): Promise<{ appV2: App }> {
|
||||||
// https://github.com/typeorm/typeorm/issues/3857
|
// https://github.com/typeorm/typeorm/issues/3857
|
||||||
|
|
@ -94,7 +106,7 @@ export class AppImportExportService {
|
||||||
.createQueryBuilder(App, 'apps')
|
.createQueryBuilder(App, 'apps')
|
||||||
.where('apps.id = :id AND apps.organization_id = :organizationId', {
|
.where('apps.id = :id AND apps.organization_id = :organizationId', {
|
||||||
id,
|
id,
|
||||||
organizationId: user.organizationId,
|
organizationId: user?.organizationId,
|
||||||
});
|
});
|
||||||
const appToExport = await queryForAppToExport.getOne();
|
const appToExport = await queryForAppToExport.getOne();
|
||||||
|
|
||||||
|
|
@ -123,7 +135,7 @@ export class AppImportExportService {
|
||||||
const appEnvironments = await manager
|
const appEnvironments = await manager
|
||||||
.createQueryBuilder(AppEnvironment, 'app_environments')
|
.createQueryBuilder(AppEnvironment, 'app_environments')
|
||||||
.where('app_environments.organizationId = :organizationId', {
|
.where('app_environments.organizationId = :organizationId', {
|
||||||
organizationId: user.organizationId,
|
organizationId: user?.organizationId,
|
||||||
})
|
})
|
||||||
.orderBy('app_environments.createdAt', 'ASC')
|
.orderBy('app_environments.createdAt', 'ASC')
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
@ -184,13 +196,13 @@ export class AppImportExportService {
|
||||||
const components =
|
const components =
|
||||||
pages.length > 0
|
pages.length > 0
|
||||||
? await manager
|
? await manager
|
||||||
.createQueryBuilder(Component, 'components')
|
.createQueryBuilder(Component, 'components')
|
||||||
.leftJoinAndSelect('components.layouts', 'layouts')
|
.leftJoinAndSelect('components.layouts', 'layouts')
|
||||||
.where('components.pageId IN(:...pageId)', {
|
.where('components.pageId IN(:...pageId)', {
|
||||||
pageId: pages.map((v) => v.id),
|
pageId: pages.map((v) => v.id),
|
||||||
})
|
})
|
||||||
.orderBy('components.created_at', 'ASC')
|
.orderBy('components.created_at', 'ASC')
|
||||||
.getMany()
|
.getMany()
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const events = await manager
|
const events = await manager
|
||||||
|
|
@ -340,8 +352,8 @@ export class AppImportExportService {
|
||||||
return await catchDbException(async () => {
|
return await catchDbException(async () => {
|
||||||
const importedApp = manager.create(App, {
|
const importedApp = manager.create(App, {
|
||||||
name: appParams.name,
|
name: appParams.name,
|
||||||
organizationId: user.organizationId,
|
organizationId: user?.organizationId,
|
||||||
userId: user.id,
|
userId: user.id, //fetch super admin user id for EE
|
||||||
slug: null,
|
slug: null,
|
||||||
icon: appParams.icon,
|
icon: appParams.icon,
|
||||||
creationMode: `${isGitApp ? 'GIT' : 'DEFAULT'}`,
|
creationMode: `${isGitApp ? 'GIT' : 'DEFAULT'}`,
|
||||||
|
|
@ -762,7 +774,7 @@ export class AppImportExportService {
|
||||||
|
|
||||||
const { dataQueryMapping } = await this.createDataQueriesForAppVersion(
|
const { dataQueryMapping } = await this.createDataQueriesForAppVersion(
|
||||||
manager,
|
manager,
|
||||||
user.organizationId,
|
user?.organizationId,
|
||||||
importingDataQueriesForAppVersion,
|
importingDataQueriesForAppVersion,
|
||||||
importingDataSource,
|
importingDataSource,
|
||||||
dataSourceForAppVersion,
|
dataSourceForAppVersion,
|
||||||
|
|
@ -1059,10 +1071,10 @@ export class AppImportExportService {
|
||||||
const options =
|
const options =
|
||||||
importingDataSource.kind === 'tooljetdb'
|
importingDataSource.kind === 'tooljetdb'
|
||||||
? this.replaceTooljetDbTableIds(
|
? this.replaceTooljetDbTableIds(
|
||||||
importingQuery.options,
|
importingQuery.options,
|
||||||
externalResourceMappings['tooljet_database'],
|
externalResourceMappings['tooljet_database'],
|
||||||
organizationId
|
organizationId
|
||||||
)
|
)
|
||||||
: importingQuery.options;
|
: importingQuery.options;
|
||||||
|
|
||||||
const newQuery = manager.create(DataQuery, {
|
const newQuery = manager.create(DataQuery, {
|
||||||
|
|
@ -1153,7 +1165,7 @@ export class AppImportExportService {
|
||||||
appResourceMappings: AppResourceMappings
|
appResourceMappings: AppResourceMappings
|
||||||
) {
|
) {
|
||||||
const defaultDataSourceIds = await this.createDefaultDataSourceForVersion(
|
const defaultDataSourceIds = await this.createDefaultDataSourceForVersion(
|
||||||
user.organizationId,
|
user?.organizationId,
|
||||||
appResourceMappings.appVersionMapping[appVersion.id],
|
appResourceMappings.appVersionMapping[appVersion.id],
|
||||||
DefaultDataSourceKinds,
|
DefaultDataSourceKinds,
|
||||||
manager
|
manager
|
||||||
|
|
@ -1192,7 +1204,7 @@ export class AppImportExportService {
|
||||||
kind: dataSource.kind,
|
kind: dataSource.kind,
|
||||||
type: DataSourceTypes.DEFAULT,
|
type: DataSourceTypes.DEFAULT,
|
||||||
scope: 'global',
|
scope: 'global',
|
||||||
organizationId: user.organizationId,
|
organizationId: user?.organizationId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -1203,7 +1215,7 @@ export class AppImportExportService {
|
||||||
kind: dataSource.kind,
|
kind: dataSource.kind,
|
||||||
type: In([DataSourceTypes.DEFAULT, DataSourceTypes.SAMPLE]),
|
type: In([DataSourceTypes.DEFAULT, DataSourceTypes.SAMPLE]),
|
||||||
scope: 'global',
|
scope: 'global',
|
||||||
organizationId: user.organizationId,
|
organizationId: user?.organizationId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -1221,7 +1233,7 @@ export class AppImportExportService {
|
||||||
|
|
||||||
if (plugin) {
|
if (plugin) {
|
||||||
const newDataSource = manager.create(DataSource, {
|
const newDataSource = manager.create(DataSource, {
|
||||||
organizationId: user.organizationId,
|
organizationId: user?.organizationId,
|
||||||
name: dataSource.name,
|
name: dataSource.name,
|
||||||
kind: dataSource.kind,
|
kind: dataSource.kind,
|
||||||
type: DataSourceTypes.DEFAULT,
|
type: DataSourceTypes.DEFAULT,
|
||||||
|
|
@ -1236,7 +1248,7 @@ export class AppImportExportService {
|
||||||
|
|
||||||
const createNewGlobalDs = async (ds: DataSource): Promise<DataSource> => {
|
const createNewGlobalDs = async (ds: DataSource): Promise<DataSource> => {
|
||||||
const newDataSource = manager.create(DataSource, {
|
const newDataSource = manager.create(DataSource, {
|
||||||
organizationId: user.organizationId,
|
organizationId: user?.organizationId,
|
||||||
name: dataSource.name,
|
name: dataSource.name,
|
||||||
kind: dataSource.kind,
|
kind: dataSource.kind,
|
||||||
type: DataSourceTypes.DEFAULT,
|
type: DataSourceTypes.DEFAULT,
|
||||||
|
|
@ -1264,7 +1276,7 @@ export class AppImportExportService {
|
||||||
) {
|
) {
|
||||||
appResourceMappings = { ...appResourceMappings };
|
appResourceMappings = { ...appResourceMappings };
|
||||||
const currentOrgEnvironments = await this.appEnvironmentUtilService.getAll(
|
const currentOrgEnvironments = await this.appEnvironmentUtilService.getAll(
|
||||||
user.organizationId,
|
user?.organizationId,
|
||||||
appVersion.appId,
|
appVersion.appId,
|
||||||
manager
|
manager
|
||||||
);
|
);
|
||||||
|
|
@ -1326,7 +1338,7 @@ export class AppImportExportService {
|
||||||
appResourceMappings = { ...appResourceMappings };
|
appResourceMappings = { ...appResourceMappings };
|
||||||
const { appVersionMapping, appDefaultEnvironmentMapping } = appResourceMappings;
|
const { appVersionMapping, appDefaultEnvironmentMapping } = appResourceMappings;
|
||||||
const organization: Organization = await manager.findOne(Organization, {
|
const organization: Organization = await manager.findOne(Organization, {
|
||||||
where: { id: user.organizationId },
|
where: { id: user?.organizationId },
|
||||||
relations: ['appEnvironments'],
|
relations: ['appEnvironments'],
|
||||||
});
|
});
|
||||||
let currentEnvironmentId: string;
|
let currentEnvironmentId: string;
|
||||||
|
|
@ -1545,7 +1557,7 @@ export class AppImportExportService {
|
||||||
|
|
||||||
// Create default data sources
|
// Create default data sources
|
||||||
const defaultDataSourceIds = await this.createDefaultDataSourceForVersion(
|
const defaultDataSourceIds = await this.createDefaultDataSourceForVersion(
|
||||||
user.organizationId,
|
user?.organizationId,
|
||||||
version.id,
|
version.id,
|
||||||
DefaultDataSourceKinds,
|
DefaultDataSourceKinds,
|
||||||
manager
|
manager
|
||||||
|
|
@ -1553,7 +1565,7 @@ export class AppImportExportService {
|
||||||
let envIdArray: string[] = [];
|
let envIdArray: string[] = [];
|
||||||
|
|
||||||
const organization: Organization = await manager.findOne(Organization, {
|
const organization: Organization = await manager.findOne(Organization, {
|
||||||
where: { id: user.organizationId },
|
where: { id: user?.organizationId },
|
||||||
relations: ['appEnvironments'],
|
relations: ['appEnvironments'],
|
||||||
});
|
});
|
||||||
envIdArray = [...organization.appEnvironments.map((env) => env.id)];
|
envIdArray = [...organization.appEnvironments.map((env) => env.id)];
|
||||||
|
|
@ -1562,7 +1574,7 @@ export class AppImportExportService {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
defaultAppEnvironments.map(async (en) => {
|
defaultAppEnvironments.map(async (en) => {
|
||||||
const env = manager.create(AppEnvironment, {
|
const env = manager.create(AppEnvironment, {
|
||||||
organizationId: user.organizationId,
|
organizationId: user?.organizationId,
|
||||||
name: en.name,
|
name: en.name,
|
||||||
isDefault: en.isDefault,
|
isDefault: en.isDefault,
|
||||||
priority: en.priority,
|
priority: en.priority,
|
||||||
|
|
@ -1627,10 +1639,10 @@ export class AppImportExportService {
|
||||||
options:
|
options:
|
||||||
dataSourceId == defaultDataSourceIds['tooljetdb']
|
dataSourceId == defaultDataSourceIds['tooljetdb']
|
||||||
? this.replaceTooljetDbTableIds(
|
? this.replaceTooljetDbTableIds(
|
||||||
query.options,
|
query.options,
|
||||||
externalResourceMappings['tooljet_database'],
|
externalResourceMappings['tooljet_database'],
|
||||||
user.organizationId
|
user?.organizationId
|
||||||
)
|
)
|
||||||
: query.options,
|
: query.options,
|
||||||
});
|
});
|
||||||
await manager.save(newQuery);
|
await manager.save(newQuery);
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,9 @@ export class ComponentsService implements IComponentsService {
|
||||||
if (componentData.type === 'Table' && _.isArray(objValue)) {
|
if (componentData.type === 'Table' && _.isArray(objValue)) {
|
||||||
return srcValue;
|
return srcValue;
|
||||||
} else if (
|
} else if (
|
||||||
(componentData.type === 'DropdownV2' || componentData.type === 'MultiselectV2') &&
|
(componentData.type === 'DropdownV2' ||
|
||||||
|
componentData.type === 'MultiselectV2' ||
|
||||||
|
componentData.type === 'Steps') &&
|
||||||
_.isArray(objValue)
|
_.isArray(objValue)
|
||||||
) {
|
) {
|
||||||
return _.isArray(srcValue) ? srcValue : Object.values(srcValue);
|
return _.isArray(srcValue) ? srcValue : Object.values(srcValue);
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,38 @@ export const stepsConfig = {
|
||||||
description: 'Step-by-step navigation aid',
|
description: 'Step-by-step navigation aid',
|
||||||
component: 'Steps',
|
component: 'Steps',
|
||||||
properties: {
|
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: {
|
steps: {
|
||||||
type: 'code',
|
type: 'code',
|
||||||
displayName: 'Steps',
|
displayName: '',
|
||||||
|
showLabel: false,
|
||||||
validation: {
|
validation: {
|
||||||
schema: {
|
schema: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
element: { type: 'object', object: { id: { type: 'number' } } },
|
element: { type: 'object' },
|
||||||
},
|
},
|
||||||
defaultValue: `[{ name: 'step 1'}, {name: 'step 2'}]`,
|
defaultValue: `[{ name: 'step 1'}, {name: 'step 2'}]`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
currentStep: {
|
|
||||||
type: 'code',
|
|
||||||
displayName: 'Current step',
|
|
||||||
validation: {
|
|
||||||
schema: { type: 'number' },
|
|
||||||
defaultValue: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stepsSelectable: {
|
stepsSelectable: {
|
||||||
type: 'toggle',
|
type: 'toggle',
|
||||||
displayName: 'Steps selectable',
|
displayName: 'Steps selectable',
|
||||||
|
|
@ -30,6 +43,36 @@ export const stepsConfig = {
|
||||||
schema: { type: 'boolean' },
|
schema: { type: 'boolean' },
|
||||||
defaultValue: false,
|
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: {
|
defaultSize: {
|
||||||
|
|
@ -40,46 +83,126 @@ export const stepsConfig = {
|
||||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
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: {
|
events: {
|
||||||
onSelect: { displayName: 'On select' },
|
onSelect: { displayName: 'On select' },
|
||||||
},
|
},
|
||||||
styles: {
|
styles: {
|
||||||
color: {
|
incompletedAccent: {
|
||||||
type: 'colorSwatches',
|
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: {
|
validation: {
|
||||||
schema: { type: 'string' },
|
schema: { type: 'string' },
|
||||||
defaultValue: 'var(--primary-brand)',
|
defaultValue: 'var(--primary-brand)',
|
||||||
},
|
},
|
||||||
|
accordian: 'steps',
|
||||||
},
|
},
|
||||||
textColor: {
|
completedLabel: {
|
||||||
type: 'colorSwatches',
|
type: 'colorSwatches',
|
||||||
displayName: 'Text color',
|
displayName: 'Completed label',
|
||||||
validation: {
|
validation: {
|
||||||
schema: { type: 'string' },
|
schema: { type: 'string' },
|
||||||
defaultValue: '#000000',
|
defaultValue: '#1B1F24',
|
||||||
},
|
},
|
||||||
|
accordian: 'steps',
|
||||||
},
|
},
|
||||||
theme: {
|
currentStepLabel: {
|
||||||
type: 'select',
|
type: 'colorSwatches',
|
||||||
displayName: 'Theme',
|
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: [
|
options: [
|
||||||
{ name: 'titles', value: 'titles' },
|
{ displayName: 'Default', value: 'default' },
|
||||||
{ name: 'numbers', value: 'numbers' },
|
{ displayName: 'None', value: 'none' },
|
||||||
{ name: 'plain', value: 'plain' },
|
|
||||||
],
|
],
|
||||||
validation: {
|
accordian: 'container',
|
||||||
schema: { type: 'string' },
|
|
||||||
defaultValue: 'titles',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
visibility: {
|
|
||||||
type: 'toggle',
|
|
||||||
displayName: 'Visibility',
|
|
||||||
validation: {
|
|
||||||
schema: { type: 'boolean' },
|
|
||||||
defaultValue: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
exposedVariables: {
|
exposedVariables: {
|
||||||
|
|
@ -92,17 +215,35 @@ export const stepsConfig = {
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
steps: {
|
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}}' },
|
currentStep: { value: '{{3}}' },
|
||||||
stepsSelectable: { value: true },
|
stepsSelectable: { value: true },
|
||||||
|
advanced: { value: `{{false}}` },
|
||||||
|
visibility: { value: '{{true}}' },
|
||||||
},
|
},
|
||||||
events: [],
|
events: [],
|
||||||
styles: {
|
styles: {
|
||||||
visibility: { value: '{{true}}' },
|
visibility: { value: '{{true}}' },
|
||||||
theme: { value: 'titles' },
|
// color: { value: '' },
|
||||||
color: { value: 'var(--primary-brand)' },
|
// textColor: { value: '' },
|
||||||
textColor: { value: '' },
|
padding: { value: 'default' },
|
||||||
|
incompletedAccent: { value: '#E4E7EB' },
|
||||||
|
incompletedLabel: { value: '#1B1F24' },
|
||||||
|
completedAccent: { value: '#4368E3' },
|
||||||
|
completedLabel: { value: '#1B1F24' },
|
||||||
|
currentStepLabel: { value: '#1B1F24' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ import { DataSourcesRepository } from '@modules/data-sources/repository';
|
||||||
import { IAppsUtilService } from './interfaces/IUtilService';
|
import { IAppsUtilService } from './interfaces/IUtilService';
|
||||||
import { DataSourcesUtilService } from '@modules/data-sources/util.service';
|
import { DataSourcesUtilService } from '@modules/data-sources/util.service';
|
||||||
import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
|
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()
|
@Injectable()
|
||||||
export class AppsUtilService implements IAppsUtilService {
|
export class AppsUtilService implements IAppsUtilService {
|
||||||
|
|
@ -487,7 +490,7 @@ export class AppsUtilService implements IAppsUtilService {
|
||||||
if (['Table'].includes(currentComponentData?.component?.component) && isArray(objValue)) {
|
if (['Table'].includes(currentComponentData?.component?.component) && isArray(objValue)) {
|
||||||
return srcValue;
|
return srcValue;
|
||||||
} else if (
|
} else if (
|
||||||
['DropdownV2', 'MultiselectV2'].includes(currentComponentData?.component?.component) &&
|
['DropdownV2', 'MultiselectV2', 'Steps'].includes(currentComponentData?.component?.component) &&
|
||||||
isArray(objValue)
|
isArray(objValue)
|
||||||
) {
|
) {
|
||||||
return isArray(srcValue) ? srcValue : Object.values(srcValue);
|
return isArray(srcValue) ? srcValue : Object.values(srcValue);
|
||||||
|
|
@ -522,4 +525,45 @@ export class AppsUtilService implements IAppsUtilService {
|
||||||
|
|
||||||
return components;
|
return components;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findAllOrganizationApps(organizationId: string): Promise<WorkspaceAppsResponseDto[]> {
|
||||||
|
return await this.appRepository.findAllOrganizationApps(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findTooljetDbTables(appId: string): Promise<{ table_id: string }[]> {
|
||||||
|
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||||
|
const tooljetDbDataQueries = await manager
|
||||||
|
.createQueryBuilder(DataQuery, 'data_queries')
|
||||||
|
.innerJoin(DataSource, 'data_sources', 'data_queries.data_source_id = data_sources.id')
|
||||||
|
.innerJoin(AppVersion, 'app_versions', 'app_versions.id = data_sources.app_version_id')
|
||||||
|
.where('app_versions.app_id = :appId', { appId })
|
||||||
|
.andWhere('data_sources.kind = :kind', { kind: 'tooljetdb' })
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
const uniqTableIds = new Set();
|
||||||
|
tooljetDbDataQueries.forEach((dq) => {
|
||||||
|
if (dq.options?.operation === 'join_tables') {
|
||||||
|
const joinOptions = dq.options?.join_table?.joins ?? [];
|
||||||
|
(joinOptions || []).forEach((join) => {
|
||||||
|
const { table, conditions } = join;
|
||||||
|
if (table) uniqTableIds.add(table);
|
||||||
|
conditions?.conditionsList?.forEach((condition) => {
|
||||||
|
const { leftField, rightField } = condition;
|
||||||
|
if (leftField?.table) {
|
||||||
|
uniqTableIds.add(leftField?.table);
|
||||||
|
}
|
||||||
|
if (rightField?.table) {
|
||||||
|
uniqTableIds.add(rightField?.table);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (dq.options.table_id) uniqTableIds.add(dq.options.table_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...uniqTableIds].map((table_id) => {
|
||||||
|
return { table_id };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export class ExternalApiSecurityGuard implements CanActivate {
|
||||||
throw new ForbiddenException('External API is disabled');
|
throw new ForbiddenException('External API is disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the authorization header
|
// // Check the authorization header
|
||||||
const authHeader = request.headers['authorization'];
|
const authHeader = request.headers['authorization'];
|
||||||
const externalApiAccessToken = this.configService.get<string>('EXTERNAL_API_ACCESS_TOKEN');
|
const externalApiAccessToken = this.configService.get<string>('EXTERNAL_API_ACCESS_TOKEN');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export class DataQueriesService implements IDataQueriesService {
|
||||||
protected readonly dataQueryRepository: DataQueryRepository,
|
protected readonly dataQueryRepository: DataQueryRepository,
|
||||||
protected readonly dataQueryUtilService: DataQueriesUtilService,
|
protected readonly dataQueryUtilService: DataQueriesUtilService,
|
||||||
protected readonly dataSourceRepository: DataSourcesRepository
|
protected readonly dataSourceRepository: DataSourcesRepository
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
async getAll(versionId: string) {
|
async getAll(versionId: string) {
|
||||||
const queries = await this.dataQueryRepository.getAll(versionId);
|
const queries = await this.dataQueryRepository.getAll(versionId);
|
||||||
|
|
@ -30,9 +30,6 @@ export class DataQueriesService implements IDataQueriesService {
|
||||||
|
|
||||||
// serialize
|
// serialize
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
if (query.dataSource.type === DataSourceTypes.STATIC) {
|
|
||||||
delete query['dataSourceId'];
|
|
||||||
}
|
|
||||||
delete query['dataSource'];
|
delete query['dataSource'];
|
||||||
|
|
||||||
const decamelizeQuery = decamelizeKeys(query);
|
const decamelizeQuery = decamelizeKeys(query);
|
||||||
|
|
|
||||||
|
|
@ -37,5 +37,17 @@ export const FEATURES: FeaturesConfig = {
|
||||||
license: LICENSE_FIELD.EXTERNAL_API,
|
license: LICENSE_FIELD.EXTERNAL_API,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
},
|
},
|
||||||
|
[FEATURE_KEY.GET_ALL_WORKSPACE_APPS]: {
|
||||||
|
license: LICENSE_FIELD.EXTERNAL_API,
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
|
[FEATURE_KEY.IMPORT_APP]: {
|
||||||
|
license: LICENSE_FIELD.EXTERNAL_API,
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
|
[FEATURE_KEY.EXPORT_APP]: {
|
||||||
|
license: LICENSE_FIELD.EXTERNAL_API,
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,41 @@ export enum FEATURE_KEY {
|
||||||
UPDATE_USER_WORKSPACE = 'UPDATE_USER_WORKSPACE',
|
UPDATE_USER_WORKSPACE = 'UPDATE_USER_WORKSPACE',
|
||||||
GET_ALL_WORKSPACES = 'GET_ALL_WORKSPACES',
|
GET_ALL_WORKSPACES = 'GET_ALL_WORKSPACES',
|
||||||
UPDATE_USER_ROLE = 'UPDATE_USER_ROLE',
|
UPDATE_USER_ROLE = 'UPDATE_USER_ROLE',
|
||||||
|
GET_ALL_WORKSPACE_APPS = 'GET_ALL_WORKSPACE_APPS',
|
||||||
|
IMPORT_APP = 'IMPORT_APP',
|
||||||
|
EXPORT_APP = 'EXPORT_APP',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DefaultDataSourceKind = 'restapi' | 'runjs' | 'runpy' | 'tooljetdb' | 'workflows';
|
||||||
|
export type NewRevampedComponent =
|
||||||
|
| 'Text'
|
||||||
|
| 'TextInput'
|
||||||
|
| 'PasswordInput'
|
||||||
|
| 'NumberInput'
|
||||||
|
| 'Table'
|
||||||
|
| 'Button'
|
||||||
|
| 'Checkbox';
|
||||||
|
export type DefaultDataSourceName =
|
||||||
|
| 'restapidefault'
|
||||||
|
| 'runjsdefault'
|
||||||
|
| 'runpydefault'
|
||||||
|
| 'tooljetdbdefault'
|
||||||
|
| 'workflowsdefault';
|
||||||
|
|
||||||
|
export const DefaultDataSourceKinds: DefaultDataSourceKind[] = ['restapi', 'runjs', 'runpy', 'tooljetdb', 'workflows'];
|
||||||
|
export const DefaultDataSourceNames: DefaultDataSourceName[] = [
|
||||||
|
'restapidefault',
|
||||||
|
'runjsdefault',
|
||||||
|
'runpydefault',
|
||||||
|
'tooljetdbdefault',
|
||||||
|
'workflowsdefault',
|
||||||
|
];
|
||||||
|
export const NewRevampedComponents: NewRevampedComponent[] = [
|
||||||
|
'Text',
|
||||||
|
'TextInput',
|
||||||
|
'PasswordInput',
|
||||||
|
'NumberInput',
|
||||||
|
'Table',
|
||||||
|
'Checkbox',
|
||||||
|
'Button',
|
||||||
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Controller, Get, Param, UseGuards, Body, Patch, Post, Put, NotFoundException } from '@nestjs/common';
|
import { 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 { UpdateUserDto, WorkspaceDto, UpdateGivenWorkspaceDto, CreateUserDto } from './dto';
|
||||||
import { IExternalApisController } from './Interfaces/IController';
|
import { IExternalApisController } from './Interfaces/IController';
|
||||||
import { EditUserRoleDto } from '@modules/roles/dto';
|
import { EditUserRoleDto } from '@modules/roles/dto';
|
||||||
|
import { ExternalApiSecurityGuard } from '@modules/auth/guards/external-api-security.guard';
|
||||||
|
|
||||||
@Controller('ext')
|
@Controller('ext')
|
||||||
export class ExternalApisController implements IExternalApisController {
|
export class ExternalApisController implements IExternalApisController {
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,13 @@ import {
|
||||||
MaxLength,
|
MaxLength,
|
||||||
ValidateIf,
|
ValidateIf,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
|
IsDefined,
|
||||||
|
IsObject,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Transform, Type } from 'class-transformer';
|
||||||
import { USER_ROLE } from '@modules/group-permissions/constants';
|
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 {
|
export enum Status {
|
||||||
ACTIVE = 'active',
|
ACTIVE = 'active',
|
||||||
ARCHIVED = 'archived',
|
ARCHIVED = 'archived',
|
||||||
|
|
@ -131,3 +134,73 @@ export class UpdateUserWorkspaceDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
groups?: GroupDto[];
|
groups?: GroupDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class VersionDto {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
createdAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AppWithVersionsDto {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
createdAt: Date;
|
||||||
|
organizationId: string;
|
||||||
|
versions: VersionDto[];
|
||||||
|
versionCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorkspaceAppsResponseDto {
|
||||||
|
apps: AppWithVersionsDto[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AppImportRequestDto {
|
||||||
|
@IsString()
|
||||||
|
tooljet_version: string;
|
||||||
|
|
||||||
|
// TODO: Add transformation and validation for app similar to tooljet_database
|
||||||
|
@IsOptional()
|
||||||
|
app: AppImportDto[];
|
||||||
|
|
||||||
|
// Optional parameter -> To be provided in import request to import app with custom name.
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
appName: string;
|
||||||
|
|
||||||
|
// TJ-DB field
|
||||||
|
@IsOptional()
|
||||||
|
// Transform the input data to the latest schema version
|
||||||
|
// This should be applied first to ensure the data is in
|
||||||
|
// the correct format before validation
|
||||||
|
@Transform(TjdbSchemaToLatestVersion)
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
// Ensure each item is properly instantiated as ImportTooljetDatabaseDto
|
||||||
|
// This is crucial for nested validation to work correctly
|
||||||
|
@Type(() => ImportTooljetDatabaseDto)
|
||||||
|
// Custom validator to check against the tooljet database schema
|
||||||
|
// This should be applied last to validate the transformed
|
||||||
|
// and instantiated data
|
||||||
|
@ValidateTooljetDatabaseImportSchema({ each: true })
|
||||||
|
tooljet_database: ImportTooljetDatabaseDto[];
|
||||||
|
}
|
||||||
|
export class AppImportDto {
|
||||||
|
@IsDefined()
|
||||||
|
@IsObject()
|
||||||
|
definition: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImportTooljetDatabaseDto {
|
||||||
|
@IsUUID()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
table_name: string;
|
||||||
|
|
||||||
|
@IsDefined()
|
||||||
|
schema: any;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// data: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ExternalApiSecurityGuard implements CanActivate {
|
|
||||||
constructor(protected configService: ConfigService) {}
|
|
||||||
|
|
||||||
canActivate(context: ExecutionContext): boolean {
|
|
||||||
const request = context.switchToHttp().getRequest();
|
|
||||||
|
|
||||||
// Check if external API is enabled
|
|
||||||
const isExternalApiEnabled = this.configService.get<string>('ENABLE_EXTERNAL_API') === 'true';
|
|
||||||
if (!isExternalApiEnabled) {
|
|
||||||
throw new ForbiddenException('External API is disabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the authorization header
|
|
||||||
const authHeader = request.headers['authorization'];
|
|
||||||
const externalApiAccessToken = this.configService.get<string>('EXTERNAL_API_ACCESS_TOKEN');
|
|
||||||
|
|
||||||
if (!authHeader || authHeader !== `Basic ${externalApiAccessToken}`) {
|
|
||||||
throw new ForbiddenException('Unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,8 +3,12 @@ import { GroupPermissionsModule } from '@modules/group-permissions/module';
|
||||||
import { RolesModule } from '@modules/roles/module';
|
import { RolesModule } from '@modules/roles/module';
|
||||||
import { DynamicModule } from '@nestjs/common';
|
import { DynamicModule } from '@nestjs/common';
|
||||||
import { getImportPath } from '@modules/app/constants';
|
import { getImportPath } from '@modules/app/constants';
|
||||||
import { ExternalApiSecurityGuard } from './guards/external-api-security.guard';
|
|
||||||
import { RolesRepository } from '@modules/roles/repository';
|
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 {
|
export class ExternalApiModule {
|
||||||
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||||
const importPath = await getImportPath(configs?.IS_GET_CONTEXT);
|
const importPath = await getImportPath(configs?.IS_GET_CONTEXT);
|
||||||
|
|
@ -14,14 +18,16 @@ export class ExternalApiModule {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
module: ExternalApiModule,
|
module: ExternalApiModule,
|
||||||
imports: [await RolesModule.register(configs), await GroupPermissionsModule.register(configs)],
|
imports: [
|
||||||
providers: [
|
await UsersModule.register(configs),
|
||||||
ExternalApiUtilService,
|
await RolesModule.register(configs),
|
||||||
ExternalApisService,
|
await GroupPermissionsModule.register(configs),
|
||||||
ExternalApiSecurityGuard,
|
await TooljetDbModule.register(configs),
|
||||||
FeatureAbilityFactory,
|
await AppsModule.register(configs),
|
||||||
RolesRepository,
|
await OrganizationsModule.register(configs),
|
||||||
|
await VersionModule.register(configs),
|
||||||
],
|
],
|
||||||
|
providers: [ExternalApiUtilService, ExternalApisService, FeatureAbilityFactory, RolesRepository],
|
||||||
controllers: [ExternalApisController],
|
controllers: [ExternalApisController],
|
||||||
exports: [ExternalApiUtilService],
|
exports: [ExternalApiUtilService],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ interface Features {
|
||||||
[FEATURE_KEY.UPDATE_USER_WORKSPACE]: FeatureConfig;
|
[FEATURE_KEY.UPDATE_USER_WORKSPACE]: FeatureConfig;
|
||||||
[FEATURE_KEY.GET_ALL_WORKSPACES]: FeatureConfig;
|
[FEATURE_KEY.GET_ALL_WORKSPACES]: FeatureConfig;
|
||||||
[FEATURE_KEY.UPDATE_USER_ROLE]: 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 {
|
export interface FeaturesConfig {
|
||||||
|
|
@ -22,3 +25,13 @@ export interface ValidateEditUserGroupAdditionObject {
|
||||||
groupsToAddIds: string[];
|
groupsToAddIds: string[];
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppResourceMappings {
|
||||||
|
defaultDataSourceIdMapping: Record<string, string>;
|
||||||
|
dataQueryMapping: Record<string, string>;
|
||||||
|
appVersionMapping: Record<string, string>;
|
||||||
|
appEnvironmentMapping: Record<string, string>;
|
||||||
|
appDefaultEnvironmentMapping: Record<string, string[]>;
|
||||||
|
pagesMapping: Record<string, string>;
|
||||||
|
componentsMapping: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface IOrganizationUtilService {
|
||||||
|
validateWorkspaceExists(workspaceId: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { DynamicModule } from '@nestjs/common';
|
||||||
import { getImportPath } from '@modules/app/constants';
|
import { getImportPath } from '@modules/app/constants';
|
||||||
import { InstanceSettingsModule } from '@modules/instance-settings/module';
|
import { InstanceSettingsModule } from '@modules/instance-settings/module';
|
||||||
import { OrganizationRepository } from './repository';
|
import { OrganizationRepository } from './repository';
|
||||||
|
import { AppEnvironmentsModule } from '@modules/app-environments/module';
|
||||||
|
|
||||||
export class OrganizationsModule {
|
export class OrganizationsModule {
|
||||||
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
|
||||||
|
|
@ -9,13 +10,14 @@ export class OrganizationsModule {
|
||||||
const { OrganizationsService } = await import(`${importPath}/organizations/service`);
|
const { OrganizationsService } = await import(`${importPath}/organizations/service`);
|
||||||
const { OrganizationsController } = await import(`${importPath}/organizations/controller`);
|
const { OrganizationsController } = await import(`${importPath}/organizations/controller`);
|
||||||
const { FeatureAbilityFactory } = await import(`${importPath}/organizations/ability`);
|
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 {
|
return {
|
||||||
module: OrganizationsModule,
|
module: OrganizationsModule,
|
||||||
imports: [await InstanceSettingsModule.register(configs)],
|
imports: [await InstanceSettingsModule.register(configs), await AppEnvironmentsModule.register(configs)],
|
||||||
controllers: [OrganizationsController],
|
controllers: [OrganizationsController],
|
||||||
providers: [OrganizationsService, OrganizationRepository, FeatureAbilityFactory, AppEnvironmentUtilService],
|
providers: [OrganizationsService, OrganizationRepository, FeatureAbilityFactory, OrganizationsUtilService],
|
||||||
|
exports: [OrganizationsUtilService],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
server/src/modules/organizations/util.service.ts
Normal file
18
server/src/modules/organizations/util.service.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { OrganizationRepository } from './repository';
|
||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
import { IOrganizationUtilService } from './interfaces/IUtilService';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OrganizationsUtilService implements IOrganizationUtilService {
|
||||||
|
constructor(protected readonly organizationRepository: OrganizationRepository) {}
|
||||||
|
|
||||||
|
async validateWorkspaceExists(workspaceId: string) {
|
||||||
|
const existingWorkspace = await this.organizationRepository.findOne({
|
||||||
|
where: { id: workspaceId },
|
||||||
|
});
|
||||||
|
if (!existingWorkspace) {
|
||||||
|
throw new BadRequestException(`Invalid workspaceId: ${workspaceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { EditUserRoleDto } from './dto';
|
import { EditUserRoleDto } from './dto';
|
||||||
import { RolesUtilService } from './util.service';
|
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 { RolesRepository } from './repository';
|
||||||
import { IRolesService } from './interfaces/IService';
|
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()
|
@Injectable()
|
||||||
export class RolesService implements IRolesService {
|
export class RolesService implements IRolesService {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ export class UsersModule {
|
||||||
imports: [await SessionModule.register(configs)],
|
imports: [await SessionModule.register(configs)],
|
||||||
controllers: [UsersController],
|
controllers: [UsersController],
|
||||||
providers: [UsersService, UserRepository, UsersUtilService, FeatureAbilityFactory],
|
providers: [UsersService, UserRepository, UsersUtilService, FeatureAbilityFactory],
|
||||||
|
exports: [UsersUtilService],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { Organization } from '@entities/organization.entity';
|
||||||
import { OrganizationUser } from '@entities/organization_user.entity';
|
import { OrganizationUser } from '@entities/organization_user.entity';
|
||||||
import { isSuperAdmin } from '@helpers/utils.helper';
|
import { isSuperAdmin } from '@helpers/utils.helper';
|
||||||
import * as uuid from 'uuid';
|
import * as uuid from 'uuid';
|
||||||
|
import { USER_ROLE } from '@modules/group-permissions/constants';
|
||||||
|
|
||||||
type UserFilterOptions = { searchText?: string; status?: string; page?: number };
|
type UserFilterOptions = { searchText?: string; status?: string; page?: number };
|
||||||
|
|
||||||
|
|
@ -168,6 +169,18 @@ export class UserRepository extends Repository<User> {
|
||||||
await manager.upsert(UserDetails, updatableParams, conflictsPaths);
|
await manager.upsert(UserDetails, updatableParams, conflictsPaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserWithAdminRole(organizationId: string, manager?: EntityManager): Promise<User | null> {
|
||||||
|
return dbTransactionWrap((manager: EntityManager) => {
|
||||||
|
return manager
|
||||||
|
.createQueryBuilder(User, 'user')
|
||||||
|
.innerJoin('user.userGroups', 'groupUsers')
|
||||||
|
.innerJoin('groupUsers.group', 'group')
|
||||||
|
.where('group.name = :groupName', { groupName: USER_ROLE.ADMIN })
|
||||||
|
.andWhere('group.organizationId = :organizationId', { organizationId })
|
||||||
|
.getOne();
|
||||||
|
}, manager || this.manager);
|
||||||
|
}
|
||||||
|
|
||||||
async findByEmail(
|
async findByEmail(
|
||||||
email: string,
|
email: string,
|
||||||
organizationId?: string,
|
organizationId?: string,
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@ import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
|
||||||
|
|
||||||
export interface IVersionUtilService {
|
export interface IVersionUtilService {
|
||||||
updateVersion(appVersion: AppVersion, appVersionUpdateDto: AppVersionUpdateDto): Promise<void>;
|
updateVersion(appVersion: AppVersion, appVersionUpdateDto: AppVersionUpdateDto): Promise<void>;
|
||||||
|
fetchVersions(appId: string): Promise<AppVersion[]>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export class VersionModule {
|
||||||
VersionUtilService,
|
VersionUtilService,
|
||||||
FeatureAbilityFactory,
|
FeatureAbilityFactory,
|
||||||
],
|
],
|
||||||
|
exports: [VersionUtilService],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,4 +72,13 @@ export class VersionUtilService implements IVersionUtilService {
|
||||||
await this.versionRepository.update(appVersion.id, editableParams);
|
await this.versionRepository.update(appVersion.id, editableParams);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchVersions(appId: string): Promise<AppVersion[]> {
|
||||||
|
return await this.versionRepository.find({
|
||||||
|
where: { appId },
|
||||||
|
order: {
|
||||||
|
createdAt: 'DESC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue