mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 17:08:34 +00:00
Merge branch 'appbuilder/sprint-10' of https://github.com/ToolJet/ToolJet into feat/new-table-mod
This commit is contained in:
commit
a999961b63
80 changed files with 2064 additions and 433 deletions
34
.github/workflows/render-preview-deploy.yml
vendored
34
.github/workflows/render-preview-deploy.yml
vendored
|
|
@ -13,12 +13,42 @@ permissions:
|
|||
jobs:
|
||||
|
||||
# Community Edition
|
||||
|
||||
create-ce-review-app:
|
||||
if: ${{ github.event.action == 'labeled' && (github.event.label.name == 'create-ce-review-app' || github.event.label.name == 'review-app') }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Sync repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check if Forked Repository
|
||||
id: check_repo
|
||||
run: |
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||
echo "is_fork=true" >> $GITHUB_ENV
|
||||
echo "FORKED_OWNER=${{ github.event.pull_request.head.repo.owner.login }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "is_fork=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Set Repository URL
|
||||
run: |
|
||||
if [[ "$is_fork" == "true" ]]; then
|
||||
echo "REPO_URL=https://github.com/${FORKED_OWNER}/ToolJet" >> $GITHUB_ENV
|
||||
else
|
||||
echo "REPO_URL=https://github.com/ToolJet/ToolJet" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Fetch and Checkout Forked Branch
|
||||
if: env.is_fork == 'true'
|
||||
run: |
|
||||
git fetch origin pull/${{ github.event.number }}/head:${{ env.BRANCH_NAME }}
|
||||
git checkout ${{ env.BRANCH_NAME }}
|
||||
|
||||
- name: Checkout Default Branch
|
||||
if: env.is_fork == 'false'
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Creating deployment for CE
|
||||
id: create-ce-deployment
|
||||
run: |
|
||||
|
|
@ -34,7 +64,7 @@ jobs:
|
|||
"name": "ToolJet CE PR #${{ env.PR_NUMBER }}",
|
||||
"notifyOnFail": "default",
|
||||
"ownerId": "tea-caeo4bj19n072h3dddc0",
|
||||
"repo": "https://github.com/ToolJet/ToolJet",
|
||||
"repo": "'"$REPO_URL"'",
|
||||
"slug": "tooljet-ce-pr-${{ env.PR_NUMBER }}",
|
||||
"suspended": "not_suspended",
|
||||
"suspenders": [],
|
||||
|
|
|
|||
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
3.7.0
|
||||
3.8.0
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
3.7.0
|
||||
3.8.0
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ import WidgetWrapper from './WidgetWrapper';
|
|||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { addChildrenWidgetsToParent, addNewWidgetToTheEditor, computeViewerBackgroundColor } from './appCanvasUtils';
|
||||
import {
|
||||
addChildrenWidgetsToParent,
|
||||
addNewWidgetToTheEditor,
|
||||
computeViewerBackgroundColor,
|
||||
getSubContainerWidthAfterPadding,
|
||||
} from './appCanvasUtils';
|
||||
import {
|
||||
CANVAS_WIDTHS,
|
||||
NO_OF_GRIDS,
|
||||
|
|
@ -20,6 +25,7 @@ import NoComponentCanvasContainer from './NoComponentCanvasContainer';
|
|||
import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants';
|
||||
import { isPDFSupported } from '@/_helpers/appUtils';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSortedComponents from '../_hooks/useSortedComponents';
|
||||
|
||||
//TODO: Revisit the logic of height (dropRef)
|
||||
|
||||
|
|
@ -103,12 +109,7 @@ export const Container = React.memo(
|
|||
if (canvasWidth !== undefined) {
|
||||
if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2;
|
||||
if (id === 'canvas') return canvasWidth;
|
||||
if (componentType === 'Container' || componentType === 'Form') {
|
||||
return (
|
||||
canvasWidth - (2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING)
|
||||
);
|
||||
}
|
||||
return canvasWidth - 2; // Need to update this 2 to correct value for other subcontainers
|
||||
return getSubContainerWidthAfterPadding(canvasWidth, componentType, id);
|
||||
}
|
||||
return realCanvasRef?.current?.offsetWidth;
|
||||
}
|
||||
|
|
@ -146,6 +147,8 @@ export const Container = React.memo(
|
|||
[setLastCanvasClickPosition]
|
||||
);
|
||||
|
||||
const sortedComponents = useSortedComponents(components, currentLayout, id);
|
||||
|
||||
return (
|
||||
<div
|
||||
// {...(config.COMMENT_FEATURE_ENABLE && showComments && { onClick: handleAddThread })}
|
||||
|
|
@ -197,7 +200,7 @@ export const Container = React.memo(
|
|||
data-parent-type={id === 'canvas' ? 'canvas' : componentType}
|
||||
style={{ height: !showEmptyContainer ? '100%' : 'auto' }} //TODO: remove hardcoded height & canvas condition
|
||||
>
|
||||
{components.map((id) => (
|
||||
{sortedComponents.map((id) => (
|
||||
<WidgetWrapper
|
||||
id={id}
|
||||
key={id}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
const prevDragParentId = useRef(null);
|
||||
const newDragParentId = useRef(null);
|
||||
const [isGroupDragging, setIsGroupDragging] = useState(false);
|
||||
const setReorderContainerChildren = useStore((state) => state.setReorderContainerChildren, shallow);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedSet = new Set(selectedComponents);
|
||||
|
|
@ -536,6 +537,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
})
|
||||
);
|
||||
}
|
||||
setReorderContainerChildren(draggedOverElemId ?? 'canvas');
|
||||
} catch (error) {
|
||||
console.error('Error dragging group', error);
|
||||
}
|
||||
|
|
@ -696,6 +698,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
resizeData.gw = _gridWidth;
|
||||
}
|
||||
handleResizeStop([resizeData]);
|
||||
setReorderContainerChildren(currentWidget?.parent ?? 'canvas');
|
||||
} catch (error) {
|
||||
console.error('ResizeEnd error ->', error);
|
||||
}
|
||||
|
|
@ -775,6 +778,11 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
ev.target.style.transform = `translate(${posX}px, ${posY}px)`;
|
||||
});
|
||||
}
|
||||
|
||||
const groupParentId =
|
||||
boxList.find(({ id }) => id === groupResizeDataRef.current[0].target.id)?.parent ?? 'canvas';
|
||||
setReorderContainerChildren(groupParentId);
|
||||
|
||||
groupResizeDataRef.current = [];
|
||||
reloadGrid();
|
||||
} catch (error) {
|
||||
|
|
@ -841,6 +849,8 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
useStore.getState().setDraggingComponentId(null);
|
||||
isDraggingRef.current = false;
|
||||
}
|
||||
|
||||
const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent ?? 'canvas';
|
||||
prevDragParentId.current = null;
|
||||
newDragParentId.current = null;
|
||||
setDragParentId(null);
|
||||
|
|
@ -880,6 +890,12 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
// Apply transform for smooth transition
|
||||
e.target.style.transform = `translate(${left}px, ${top}px)`;
|
||||
|
||||
// Force reordering of conatiner if the parent has not changed
|
||||
const newParentId = target.slotId === 'real-canvas' ? 'canvas' : target.slotId;
|
||||
if (oldParentId === newParentId) {
|
||||
setReorderContainerChildren(newParentId);
|
||||
}
|
||||
|
||||
// Select the dragged component after drop
|
||||
setTimeout(() => setSelectedComponents([dragged.id]));
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -23,3 +23,7 @@ export const CONTAINER_FORM_CANVAS_PADDING = 7;
|
|||
export const SUBCONTAINER_CANVAS_BORDER_WIDTH = 1;
|
||||
|
||||
export const BOX_PADDING = 2;
|
||||
|
||||
export const TAB_CANVAS_PADDING = 7.5;
|
||||
|
||||
export const MODAL_CANVAS_PADDING = 5;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,16 @@ import { toast } from 'react-hot-toast';
|
|||
import _, { debounce } from 'lodash';
|
||||
import { useGridStore } from '@/_stores/gridStore';
|
||||
import { findHighestLevelofSelection } from './Grid/gridUtils';
|
||||
import { CANVAS_WIDTHS, NO_OF_GRIDS, WIDGETS_WITH_DEFAULT_CHILDREN } from './appCanvasConstants';
|
||||
import {
|
||||
CANVAS_WIDTHS,
|
||||
NO_OF_GRIDS,
|
||||
WIDGETS_WITH_DEFAULT_CHILDREN,
|
||||
CONTAINER_FORM_CANVAS_PADDING,
|
||||
SUBCONTAINER_CANVAS_BORDER_WIDTH,
|
||||
BOX_PADDING,
|
||||
TAB_CANVAS_PADDING,
|
||||
MODAL_CANVAS_PADDING,
|
||||
} from './appCanvasConstants';
|
||||
|
||||
export function snapToGrid(canvasWidth, x, y) {
|
||||
const gridX = canvasWidth / 43;
|
||||
|
|
@ -712,3 +721,25 @@ export const getSubContainerIdWithSlots = (parentId) => {
|
|||
}
|
||||
return cleanParentId;
|
||||
};
|
||||
|
||||
export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, componentId) => {
|
||||
let padding = 2; //Need to update this 2 to correct value for other subcontainers
|
||||
if (componentType === 'Container' || componentType === 'Form') {
|
||||
padding = 2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING;
|
||||
}
|
||||
if (componentType === 'Tabs') {
|
||||
padding = 2 * TAB_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING;
|
||||
}
|
||||
if (componentType === 'ModalV2') {
|
||||
const isModalHeader = componentId?.includes('header');
|
||||
if (isModalHeader) {
|
||||
const isModalHeaderCloseBtnEnabled = !useStore.getState().getResolvedComponent(componentId)?.properties
|
||||
?.hideCloseButton;
|
||||
console.log('isModalHeaderCloseBtnEnabled', isModalHeaderCloseBtnEnabled);
|
||||
padding = 2 * (MODAL_CANVAS_PADDING + (isModalHeaderCloseBtnEnabled ? 56 : 0));
|
||||
} else {
|
||||
padding = 2 * MODAL_CANVAS_PADDING;
|
||||
}
|
||||
}
|
||||
return canvasWidth - padding;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import cx from 'classnames';
|
|||
import PlusRectangle from '@/_ui/Icon/solidIcons/PlusRectangle';
|
||||
import Remove from '@/_ui/Icon/bulkIcons/Remove';
|
||||
import ParameterForm from './ParameterForm';
|
||||
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
|
||||
|
||||
const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRemove, otherParams }) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
|
@ -47,6 +48,17 @@ const ParameterDetails = ({ darkMode, onSubmit, isEdit, name, defaultValue, onRe
|
|||
}
|
||||
};
|
||||
|
||||
usePopoverObserver(
|
||||
document.getElementsByClassName('query-details')[0],
|
||||
isEdit
|
||||
? document.getElementById(`query-param-${String(name).toLowerCase()}`)
|
||||
: document.getElementById('runjs-param-add-btn'),
|
||||
document.getElementById('parameter-form-popover'),
|
||||
showModal,
|
||||
() => setShowModal(true),
|
||||
closeMenu
|
||||
);
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
trigger="click"
|
||||
|
|
|
|||
|
|
@ -219,4 +219,11 @@
|
|||
.react-datepicker__navigation{
|
||||
overflow: visible !important;
|
||||
height: inherit !important;
|
||||
}
|
||||
.tjdb-td-wrapper{
|
||||
.react-datepicker-time__input{
|
||||
input{
|
||||
line-height: normal !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import Remove from '@/_ui/Icon/bulkIcons/Remove';
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ToolTip } from '@/_components/ToolTip';
|
||||
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
|
||||
|
||||
const DropDownSelect = ({
|
||||
darkMode,
|
||||
|
|
@ -130,6 +131,15 @@ const DropDownSelect = ({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selected]);
|
||||
|
||||
usePopoverObserver(
|
||||
document.getElementsByClassName('query-details')[0],
|
||||
document.getElementById(popoverBtnId.current),
|
||||
document.getElementById(popoverId.current),
|
||||
showMenu,
|
||||
() => setShowMenu(true),
|
||||
() => setShowMenu(false)
|
||||
);
|
||||
|
||||
function checkElementPosition() {
|
||||
if (isForeignKeyInEditCell) {
|
||||
return 'bottom-start';
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import { appService } from '@/_services';
|
|||
import { deepClone } from '@/_helpers/utilities/utils.helpers';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { useEventActions, useEvents } from '@/AppBuilder/_stores/slices/eventsSlice';
|
||||
import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup';
|
||||
import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
|
||||
|
||||
export const EventManager = ({
|
||||
sourceId,
|
||||
|
|
@ -503,7 +505,7 @@ export const EventManager = ({
|
|||
)}
|
||||
|
||||
{event.actionId === 'open-webpage' && (
|
||||
<div className="p-1">
|
||||
<div>
|
||||
<label className="form-label mt-1">{t('editor.inspector.eventManager.url', 'URL')}</label>
|
||||
<CodeHinter
|
||||
type="basic"
|
||||
|
|
@ -512,6 +514,17 @@ export const EventManager = ({
|
|||
usePortalEditor={false}
|
||||
component={component}
|
||||
/>
|
||||
<div className="d-flex align-items-center justify-content-between mt-3">
|
||||
<label className="form-label mt-1">Open in</label>
|
||||
<ToggleGroup
|
||||
onValueChange={(_value) => handlerChanged(index, 'windowTarget', _value)}
|
||||
defaultValue={event?.windowTarget || 'newTab'}
|
||||
style={{ width: '74%' }}
|
||||
>
|
||||
<ToggleGroupItem value="newTab">New tab</ToggleGroupItem>
|
||||
<ToggleGroupItem value="currentTab">Current tab</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -123,6 +123,14 @@ export const buttonGroupConfig = {
|
|||
defaultValue: '#007bff',
|
||||
},
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'left',
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
selected: [1],
|
||||
|
|
@ -155,6 +163,7 @@ export const buttonGroupConfig = {
|
|||
disabledState: { value: '{{false}}' },
|
||||
selectedTextColor: { value: '#FFFFFF' },
|
||||
selectedBackgroundColor: { value: '#4368E3' },
|
||||
alignment: { value: 'left' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -143,6 +143,15 @@ export const imageConfig = {
|
|||
},
|
||||
accordian: 'Image',
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'center',
|
||||
},
|
||||
accordian: 'Image',
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Background',
|
||||
|
|
@ -179,11 +188,11 @@ export const imageConfig = {
|
|||
padding: {
|
||||
type: 'switch',
|
||||
displayName: 'Padding',
|
||||
validation: { schema: { type: 'string' }, defaultValue: 'default' },
|
||||
options: [
|
||||
{ displayName: 'Default', value: 'default' },
|
||||
{ displayName: 'Custom', value: 'custom' },
|
||||
],
|
||||
validation: { schema: { type: 'string' }, defaultValue: 'default' },
|
||||
accordian: 'Container',
|
||||
isFxNotRequired: true,
|
||||
},
|
||||
|
|
@ -244,7 +253,6 @@ export const imageConfig = {
|
|||
loadingState: { value: '{{false}}' },
|
||||
disabledState: { value: '{{false}}' },
|
||||
visibility: { value: '{{true}}' },
|
||||
visible: { value: '{{true}}' },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
|
|
@ -256,6 +264,7 @@ export const imageConfig = {
|
|||
boxShadow: { value: '0px 0px 0px 0px #00000090' },
|
||||
padding: { value: 'default' },
|
||||
customPadding: { value: '{{0}}' },
|
||||
alignment: { value: 'center' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -159,6 +159,14 @@ export const linkConfig = {
|
|||
],
|
||||
accordian: 'container',
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'left',
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {},
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -50,6 +50,14 @@ export const paginationConfig = {
|
|||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'left',
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
totalPages: null,
|
||||
|
|
@ -73,6 +81,7 @@ export const paginationConfig = {
|
|||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledState: { value: '{{false}}' },
|
||||
alignment: { value: 'left' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,6 +32,14 @@ export const svgImageConfig = {
|
|||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'left',
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
value: {},
|
||||
|
|
@ -50,6 +58,7 @@ export const svgImageConfig = {
|
|||
events: [],
|
||||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
alignment: { value: 'left' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@ export const tagsConfig = {
|
|||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'left',
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {},
|
||||
definition: {
|
||||
|
|
@ -54,6 +62,7 @@ export const tagsConfig = {
|
|||
events: [],
|
||||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
alignment: { value: 'left' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const ModalFooter = React.memo(({ id, isDisabled, customStyles, darkMode,
|
|||
overflowX: 'hidden',
|
||||
overflowY: isDisabled ? 'hidden' : 'auto',
|
||||
}}
|
||||
componentType="ModalV2"
|
||||
/>
|
||||
{isDisabled && (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const ModalHeader = React.memo(
|
|||
overflowX: 'hidden',
|
||||
overflowY: isDisabled ? 'hidden' : 'auto',
|
||||
}}
|
||||
componentType="ModalV2"
|
||||
/>
|
||||
</div>
|
||||
{isDisabled && (
|
||||
|
|
|
|||
|
|
@ -105,9 +105,10 @@ export const ModalWidget = ({ ...restProps }) => {
|
|||
<SubContainer
|
||||
id={`${id}`}
|
||||
canvasHeight={modalBodyHeight}
|
||||
styles={{ backgroundColor: customStyles.modalBody.backgroundColor, height: 'inherit' }}
|
||||
styles={{ backgroundColor: customStyles.modalBody.backgroundColor }}
|
||||
canvasWidth={modalWidth}
|
||||
darkMode={darkMode}
|
||||
componentType="ModalV2"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
var tinycolor = require('tinycolor2');
|
||||
const tinycolor = require('tinycolor2');
|
||||
import { MODAL_CANVAS_PADDING } from '@/AppBuilder/AppCanvas/appCanvasConstants';
|
||||
|
||||
export function createModalStyles({
|
||||
height,
|
||||
|
|
@ -17,25 +18,27 @@ export function createModalStyles({
|
|||
boxShadow,
|
||||
}) {
|
||||
const backwardCompatibilityCheck = height == '34' || modalHeight != undefined ? true : false;
|
||||
|
||||
return {
|
||||
modalBody: {
|
||||
height: backwardCompatibilityCheck ? computedCanvasHeight : height,
|
||||
backgroundColor:
|
||||
['#fff', '#ffffffff'].includes(bodyBackgroundColor) && darkMode ? '#1F2837' : bodyBackgroundColor,
|
||||
overflowY: isDisabledModal ? 'hidden' : 'auto',
|
||||
padding: `${MODAL_CANVAS_PADDING}px`,
|
||||
},
|
||||
modalHeader: {
|
||||
backgroundColor:
|
||||
['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor,
|
||||
height: headerHeightPx,
|
||||
overflowY: isDisabledModal ? 'hidden' : 'auto',
|
||||
padding: `${4.5}px ${MODAL_CANVAS_PADDING}px`,
|
||||
},
|
||||
modalFooter: {
|
||||
backgroundColor:
|
||||
['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor,
|
||||
height: footerHeightPx,
|
||||
overflowY: isDisabledModal ? 'hidden' : 'auto',
|
||||
padding: `${4.5}px ${MODAL_CANVAS_PADDING}px`,
|
||||
},
|
||||
buttonStyles: {
|
||||
backgroundColor: triggerButtonBackgroundColor,
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@ export default function generateColumnsData({
|
|||
return 1;
|
||||
}
|
||||
};
|
||||
} else if (columnType === 'number') {
|
||||
sortType = 'basic';
|
||||
}
|
||||
const width = columnSize || defaultColumn.width;
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useRef, useState, useEffect } from 'react';
|
|||
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
|
||||
import { resolveWidgetFieldValue, isExpectedDataType } from '@/_helpers/utils';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
|
||||
import { TAB_CANVAS_PADDING } from '@/AppBuilder/AppCanvas/appCanvasConstants';
|
||||
export const Tabs = function Tabs({
|
||||
id,
|
||||
component,
|
||||
|
|
@ -117,6 +117,7 @@ export const Tabs = function Tabs({
|
|||
position: 'absolute',
|
||||
top: parsedHideTabs ? '0px' : '41px',
|
||||
width: '100%',
|
||||
padding: TAB_CANVAS_PADDING,
|
||||
}}
|
||||
>
|
||||
<SubContainer
|
||||
|
|
@ -144,7 +145,12 @@ export const Tabs = function Tabs({
|
|||
<div
|
||||
data-disabled={parsedDisabledState}
|
||||
className="card tabs-component"
|
||||
style={{ height, display: parsedWidgetVisibility ? 'flex' : 'none', backgroundColor: bgColor, boxShadow }}
|
||||
style={{
|
||||
height,
|
||||
display: parsedWidgetVisibility ? 'flex' : 'none',
|
||||
backgroundColor: bgColor,
|
||||
boxShadow,
|
||||
}}
|
||||
data-cy={dataCy}
|
||||
>
|
||||
<ul
|
||||
|
|
|
|||
46
frontend/src/AppBuilder/_hooks/usePopoverObserver.js
Normal file
46
frontend/src/AppBuilder/_hooks/usePopoverObserver.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
|
||||
function usePopoverObserver(containerRef, triggerRef, popoverRef, show, onShow, onHide, threshold = 0.5) {
|
||||
const prevShow = useRef(false);
|
||||
|
||||
// Check if it is a ref or a DOM element
|
||||
const container = containerRef?.current ? containerRef.current : containerRef;
|
||||
const trigger = triggerRef?.current ? triggerRef.current : triggerRef;
|
||||
const popover = popoverRef?.current ? popoverRef.current : popoverRef;
|
||||
|
||||
useEffect(() => {
|
||||
if (!container || !trigger) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (prevShow.current) {
|
||||
onShow();
|
||||
prevShow.current = false;
|
||||
}
|
||||
} else if (show) {
|
||||
onHide();
|
||||
prevShow.current = true;
|
||||
}
|
||||
},
|
||||
{ root: container, threshold: [threshold] }
|
||||
);
|
||||
|
||||
observer.observe(trigger);
|
||||
|
||||
const handleOutsideClick = (event) => {
|
||||
if (popover && !popover.contains(event.target) && prevShow.current) {
|
||||
prevShow.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleOutsideClick);
|
||||
|
||||
return () => {
|
||||
observer.unobserve(trigger);
|
||||
document.removeEventListener('mousedown', handleOutsideClick);
|
||||
};
|
||||
}, [containerRef, triggerRef, popoverRef, show, onShow, onHide, threshold]);
|
||||
}
|
||||
|
||||
export default usePopoverObserver;
|
||||
49
frontend/src/AppBuilder/_hooks/useSortedComponents.js
Normal file
49
frontend/src/AppBuilder/_hooks/useSortedComponents.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { useMemo, useRef } from 'react';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
const useSortedComponents = (components, currentLayout, id) => {
|
||||
const getCurrentPageComponents = useStore((state) => state.getCurrentPageComponents, shallow);
|
||||
const reorderContainerChildren = useStore((state) => state.reorderContainerChildren, shallow);
|
||||
const prevForceUpdateRef = useRef(0);
|
||||
const prevComponentsOrder = useRef(components);
|
||||
|
||||
// Function to sort the components based on position in container for tab navigation
|
||||
const sortedComponents = useMemo(() => {
|
||||
const { triggerUpdate, containerId } = reorderContainerChildren;
|
||||
|
||||
// If a forced update occurred for a different container, return the previous order
|
||||
const isForcedUpdate = prevForceUpdateRef.current !== triggerUpdate;
|
||||
if (isForcedUpdate) {
|
||||
prevForceUpdateRef.current = triggerUpdate;
|
||||
if (containerId !== id) {
|
||||
return prevComponentsOrder.current;
|
||||
}
|
||||
}
|
||||
|
||||
const currentPageComponents = getCurrentPageComponents();
|
||||
|
||||
const newComponentsOrder = [...components].sort((a, b) => {
|
||||
const aTop = currentPageComponents?.[a]?.layouts?.[currentLayout]?.top;
|
||||
const bTop = currentPageComponents?.[b]?.layouts?.[currentLayout]?.top;
|
||||
if (aTop !== bTop) {
|
||||
return aTop - bTop;
|
||||
} else {
|
||||
const aLeft = currentPageComponents?.[a]?.layouts?.[currentLayout]?.left;
|
||||
const bLeft = currentPageComponents?.[b]?.layouts?.[currentLayout]?.left;
|
||||
if (aLeft !== bLeft) {
|
||||
return aLeft - bLeft;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
prevComponentsOrder.current = newComponentsOrder;
|
||||
return newComponentsOrder;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [components, currentLayout, reorderContainerChildren.triggerUpdate, id]);
|
||||
|
||||
return sortedComponents;
|
||||
};
|
||||
|
||||
export default useSortedComponents;
|
||||
|
|
@ -444,7 +444,7 @@ export const createEventsSlice = (set, get) => ({
|
|||
component: `[Page ${pageName}] [Component ${componentName}] [Event ${event?.eventId}] [Action ${event.actionId}]`,
|
||||
page: `[Page ${pageName}] [Event ${event.eventId}] [Action ${event.actionId}]`,
|
||||
query: `[Query ${getQueryName()}] [Event ${event.eventId}] [Action ${event.actionId}]`,
|
||||
customLog: `${event.description}`,
|
||||
customLog: `${event.key}`,
|
||||
};
|
||||
|
||||
return headerMap[source] || '';
|
||||
|
|
@ -457,7 +457,7 @@ export const createEventsSlice = (set, get) => ({
|
|||
page: 'Event Errors with page',
|
||||
component: 'Component Event',
|
||||
query: 'Event Errors with query',
|
||||
customLog: 'Custom Log',
|
||||
customLog: 'Queries',
|
||||
};
|
||||
|
||||
return errorTargetMap[source];
|
||||
|
|
@ -470,8 +470,9 @@ export const createEventsSlice = (set, get) => ({
|
|||
error: {
|
||||
message: error.message,
|
||||
description: JSON.stringify(error.message, null, 2),
|
||||
...(event.component && componentId && { componentId: componentId }),
|
||||
...(event.component === 'component' && componentId && { componentId: componentId }),
|
||||
},
|
||||
description: event?.description,
|
||||
errorTarget: constructErrorTarget(),
|
||||
options: options,
|
||||
strace: 'app_level',
|
||||
|
|
@ -584,7 +585,7 @@ export const createEventsSlice = (set, get) => ({
|
|||
//! if resolvecode default value should be the value itself not empty string ... Ask KAVIN
|
||||
const resolvedValue = getResolvedValue(event.url, customVariables);
|
||||
// const url = resolveReferences(event.url, undefined, customVariables);
|
||||
window.open(resolvedValue, '_blank');
|
||||
window.open(resolvedValue, event?.windowTarget === 'newTab' ? '_blank' : '_self');
|
||||
return Promise.resolve();
|
||||
}
|
||||
case 'go-to-app': {
|
||||
|
|
@ -1128,30 +1129,51 @@ export const createEventsSlice = (set, get) => ({
|
|||
return executeAction(event, mode, {});
|
||||
};
|
||||
|
||||
const logInfo = (log) => {
|
||||
const logInfo = (log, isFromTransformation) => {
|
||||
const query = dataQuery.queries.modules['canvas'].find((query) => query.id == queryId);
|
||||
const error = new Error();
|
||||
const stackLine = error.stack.split('\n')[2];
|
||||
const stackLine = error.stack.split('\n')[isFromTransformation ? 3 : 2];
|
||||
const lineNumberMatch = stackLine.match(/:(\d+):\d+\)$/);
|
||||
const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown';
|
||||
const event = { actionId: 'log-info', description: `${log}, Line ${lineNumber - 2}`, eventType: 'customLog' };
|
||||
const event = {
|
||||
actionId: 'log-info',
|
||||
key: `${query.name}${isFromTransformation ? ', transformation' : ''}, line ${lineNumber - 2}`,
|
||||
description: log,
|
||||
eventType: 'customLog',
|
||||
query,
|
||||
};
|
||||
return executeAction(event, mode, {});
|
||||
};
|
||||
|
||||
const logError = (log) => {
|
||||
const logError = (log, isFromTransformation = false) => {
|
||||
const query = dataQuery.queries.modules['canvas'].find((query) => query.id == queryId);
|
||||
const error = new Error();
|
||||
const stackLine = error.stack.split('\n')[2];
|
||||
const stackLine = error.stack.split('\n')[isFromTransformation ? 3 : 2];
|
||||
const lineNumberMatch = stackLine.match(/:(\d+):\d+\)$/);
|
||||
const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown';
|
||||
const event = { actionId: 'log-error', description: `${log}, Line ${lineNumber - 2}`, eventType: 'customLog' };
|
||||
const event = {
|
||||
actionId: 'log-error',
|
||||
key: `${query.name}${isFromTransformation ? ', transformation' : ''}, line ${lineNumber - 2}`,
|
||||
description: log,
|
||||
eventType: 'customLog',
|
||||
query,
|
||||
};
|
||||
return executeAction(event, mode, {});
|
||||
};
|
||||
|
||||
const log = (log) => {
|
||||
const log = (log, isFromTransformation = false) => {
|
||||
const query = dataQuery.queries.modules['canvas'].find((query) => query.id == queryId);
|
||||
const error = new Error();
|
||||
const stackLine = error.stack.split('\n')[2];
|
||||
const stackLine = error.stack.split('\n')[isFromTransformation ? 3 : 2];
|
||||
const lineNumberMatch = stackLine.match(/:(\d+):\d+\)$/);
|
||||
const lineNumber = lineNumberMatch ? lineNumberMatch[1] : 'unknown';
|
||||
const event = { actionId: 'log', description: `${log}, Line ${lineNumber - 2}`, eventType: 'customLog' };
|
||||
const event = {
|
||||
actionId: 'log',
|
||||
key: `${query.name}${isFromTransformation ? ', transformation' : ''}, line ${lineNumber - 2}`,
|
||||
description: log,
|
||||
eventType: 'customLog',
|
||||
query,
|
||||
};
|
||||
return executeAction(event, mode, {});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ const initialState = {
|
|||
lastCanvasIdClick: '',
|
||||
lastCanvasClickPosition: null,
|
||||
draggingComponentId: null,
|
||||
reorderContainerChildren: {
|
||||
containerId: null,
|
||||
triggerUpdate: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const createGridSlice = (set, get) => ({
|
||||
|
|
@ -73,4 +77,10 @@ export const createGridSlice = (set, get) => ({
|
|||
setLastCanvasClickPosition: (position) => {
|
||||
set({ lastCanvasClickPosition: position });
|
||||
},
|
||||
setReorderContainerChildren: (containerId) => {
|
||||
// Function to trigger reordering of specific container for tab navigation
|
||||
set((state) => ({
|
||||
reorderContainerChildren: { containerId, triggerUpdate: state.reorderContainerChildren.triggerUpdate + 1 },
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -446,7 +446,7 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
query,
|
||||
'edit'
|
||||
);
|
||||
if (finalData.status === 'failed') {
|
||||
if (finalData?.status === 'failed') {
|
||||
setResolvedQuery(queryId, {
|
||||
isLoading: false,
|
||||
});
|
||||
|
|
@ -650,7 +650,7 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
query,
|
||||
'edit'
|
||||
);
|
||||
if (finalData.status === 'failed') {
|
||||
if (finalData?.status === 'failed') {
|
||||
onEvent('onDataQueryFailure', queryEvents);
|
||||
setPreviewLoading(false);
|
||||
setIsPreviewQueryLoading(false);
|
||||
|
|
@ -778,18 +778,30 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
runTransformation: async (rawData, transformation, transformationLanguage = 'javascript', query, mode = 'edit') => {
|
||||
const data = rawData;
|
||||
const {
|
||||
queryPanel: { runPythonTransformation },
|
||||
queryPanel: { runPythonTransformation, createProxy },
|
||||
getResolvedState,
|
||||
} = get();
|
||||
let result = [];
|
||||
let result = {};
|
||||
const currentState = getResolvedState();
|
||||
|
||||
if (transformationLanguage === 'python') {
|
||||
result = await runPythonTransformation(currentState, data, transformation, query, mode);
|
||||
} else if (transformationLanguage === 'javascript') {
|
||||
try {
|
||||
const { eventsSlice } = get();
|
||||
const { generateAppActions } = eventsSlice;
|
||||
const queriesInResolvedState = deepClone(currentState.queries);
|
||||
const actions = generateAppActions(query?.id, mode);
|
||||
|
||||
const proxiedComponents = createProxy(currentState?.components, 'components');
|
||||
const proxiedGlobals = createProxy(currentState?.globals, 'globals');
|
||||
const proxiedConstants = createProxy(currentState?.constants, 'constants');
|
||||
const proxiedVariables = createProxy(currentState?.variables, 'variables');
|
||||
const proxiedPage = createProxy(deepClone(currentState?.page, 'page'));
|
||||
const proxiedQueriesInResolvedState = createProxy(queriesInResolvedState, 'queries');
|
||||
|
||||
const evalFunction = Function(
|
||||
['data', 'moment', '_', 'components', 'queries', 'globals', 'variables', 'page', 'constants'],
|
||||
['data', 'moment', '_', 'components', 'queries', 'globals', 'variables', 'page', 'constants', 'actions'],
|
||||
transformation
|
||||
);
|
||||
|
||||
|
|
@ -797,32 +809,51 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
data,
|
||||
moment,
|
||||
_,
|
||||
currentState.components,
|
||||
currentState.queries,
|
||||
currentState.globals,
|
||||
currentState.variables,
|
||||
currentState.page,
|
||||
currentState.constants
|
||||
proxiedComponents,
|
||||
proxiedQueriesInResolvedState,
|
||||
proxiedGlobals,
|
||||
proxiedVariables,
|
||||
proxiedPage,
|
||||
proxiedConstants,
|
||||
{
|
||||
logError: function (log) {
|
||||
return actions.logError.call(actions, log, true);
|
||||
},
|
||||
logInfo: function (log) {
|
||||
return actions.logInfo.call(actions, log, true);
|
||||
},
|
||||
log: function (log) {
|
||||
return actions.log.call(actions, log, true);
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
result = {
|
||||
message: err.stack.split('\n')[0],
|
||||
status: 'failed',
|
||||
data: data,
|
||||
};
|
||||
const stackLines = err.stack.split('\n');
|
||||
const errorLocation =
|
||||
stackLines[2]?.match(/<anonymous>:(\d+):(\d+)/) ?? stackLines[1]?.match(/<anonymous>:(\d+):(\d+)/);
|
||||
|
||||
let lineNumber = null;
|
||||
|
||||
if (errorLocation) {
|
||||
lineNumber = errorLocation[1] - 2;
|
||||
}
|
||||
|
||||
console.log('JS execution failed: ', err);
|
||||
let error = err.message || err.stack.split('\n')[0] || 'JS execution failed';
|
||||
result = { status: 'failed', data: { message: error, description: error, lineNumber } };
|
||||
get().debugger.log({
|
||||
logLevel: result?.status === 'failed' ? 'error' : 'success',
|
||||
type: 'transformation',
|
||||
kind: query.kind,
|
||||
key: `${query.name}, transformation, line ${result?.data?.lineNumber}`,
|
||||
message: result?.message,
|
||||
error: result?.data,
|
||||
isTransformation: true,
|
||||
isQuerySuccessLog: result?.status === 'failed' ? false : true,
|
||||
errorTarget: 'Queries',
|
||||
});
|
||||
}
|
||||
}
|
||||
get().debugger.log({
|
||||
logLevel: result?.status === 'failed' ? 'error' : 'success',
|
||||
type: 'transformation',
|
||||
kind: query.kind,
|
||||
key: query.name,
|
||||
message: result?.message,
|
||||
error: result,
|
||||
isTransformation: true,
|
||||
isQuerySuccessLog: result?.status === 'failed' ? false : true,
|
||||
errorTarget: 'Queries',
|
||||
});
|
||||
return result;
|
||||
},
|
||||
|
||||
|
|
@ -890,12 +921,13 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
createProxy: (obj, path = '') => {
|
||||
const { queryPanel } = get();
|
||||
const { createProxy } = queryPanel;
|
||||
|
||||
return new Proxy(obj, {
|
||||
get(target, prop) {
|
||||
const fullPath = path ? `${path}.${prop}` : prop;
|
||||
|
||||
if (!(prop in target)) {
|
||||
throw new Error(`Property "${fullPath}" is not defined`);
|
||||
throw new Error(`ReferenceError: ${fullPath} is not defined`);
|
||||
}
|
||||
|
||||
const value = target[prop];
|
||||
|
|
@ -984,13 +1016,16 @@ export const createQueryPanelSlice = (set, get) => ({
|
|||
|
||||
//Proxy Func required to get current execution line number from stack to log in debugger
|
||||
|
||||
const proxiedComponents = createProxy(deepClone(resolvedState?.components));
|
||||
const proxiedGlobals = createProxy(deepClone(resolvedState?.globals));
|
||||
const proxiedConstants = createProxy(deepClone(resolvedState?.constants));
|
||||
const proxiedVariables = createProxy(deepClone(resolvedState?.variables));
|
||||
const proxiedPage = createProxy(deepClone(resolvedState?.page));
|
||||
const proxiedQueriesInResolvedState = createProxy(deepClone(queriesInResolvedState));
|
||||
const proxiedFormattedParams = createProxy(!_.isEmpty(proxiedFormattedParams) ? [proxiedFormattedParams] : []);
|
||||
const proxiedComponents = createProxy(deepClone(resolvedState?.components), 'components');
|
||||
const proxiedGlobals = createProxy(deepClone(resolvedState?.globals), 'globals');
|
||||
const proxiedConstants = createProxy(deepClone(resolvedState?.constants), 'constants');
|
||||
const proxiedVariables = createProxy(deepClone(resolvedState?.variables), 'variables');
|
||||
const proxiedPage = createProxy(deepClone(resolvedState?.page, 'page'));
|
||||
const proxiedQueriesInResolvedState = createProxy(deepClone(queriesInResolvedState), 'queries');
|
||||
const proxiedFormattedParams = createProxy(
|
||||
!_.isEmpty(proxiedFormattedParams) ? [proxiedFormattedParams] : [],
|
||||
'params'
|
||||
);
|
||||
|
||||
const fnParams = [
|
||||
'moment',
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export const ButtonGroup = function Button({
|
|||
selectedBackgroundColor,
|
||||
selectedTextColor,
|
||||
boxShadow,
|
||||
alignment,
|
||||
} = styles;
|
||||
|
||||
const computedStyles = {
|
||||
|
|
@ -115,38 +116,53 @@ export const ButtonGroup = function Button({
|
|||
fireEvent('onClick');
|
||||
}
|
||||
};
|
||||
|
||||
const mapAlignment = (alignment) => {
|
||||
switch (alignment) {
|
||||
case 'left':
|
||||
return 'flex-start';
|
||||
case 'right':
|
||||
return 'flex-end';
|
||||
case 'center':
|
||||
return 'center';
|
||||
default:
|
||||
return 'flex-start'; // Default to left alignment if the value is unknown
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="widget-buttongroup" style={{ height }} data-cy={dataCy}>
|
||||
{label && (
|
||||
<p
|
||||
style={{ display: computedStyles.display }}
|
||||
className={`widget-buttongroup-label ${darkMode && 'text-light'}`}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
)}
|
||||
<div className="widget-buttongroup" style={{ height, alignItems: mapAlignment(alignment) }} data-cy={dataCy}>
|
||||
<div>
|
||||
{data?.map((item, index) => (
|
||||
<button
|
||||
style={{
|
||||
...computedStyles,
|
||||
backgroundColor: defaultActive?.includes(values[index]) ? selectedBackgroundColor : backgroundColor,
|
||||
color: defaultActive?.includes(values[index]) ? selectedTextColor : textColor,
|
||||
transition: 'all .1s ease',
|
||||
boxShadow,
|
||||
...(disabledState && disabledStyles),
|
||||
}}
|
||||
key={index}
|
||||
disabled={disabledState}
|
||||
className={'group-button overflow-hidden'}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
buttonClick(index);
|
||||
}}
|
||||
{label && (
|
||||
<p
|
||||
style={{ display: computedStyles.display }}
|
||||
className={`widget-buttongroup-label ${darkMode && 'text-light'}`}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
{label}
|
||||
</p>
|
||||
)}
|
||||
<div>
|
||||
{data?.map((item, index) => (
|
||||
<button
|
||||
style={{
|
||||
...computedStyles,
|
||||
backgroundColor: defaultActive?.includes(values[index]) ? selectedBackgroundColor : backgroundColor,
|
||||
color: defaultActive?.includes(values[index]) ? selectedTextColor : textColor,
|
||||
transition: 'all .1s ease',
|
||||
boxShadow,
|
||||
...(disabledState && disabledStyles),
|
||||
}}
|
||||
key={index}
|
||||
disabled={disabledState}
|
||||
className={'group-button overflow-hidden'}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
buttonClick(index);
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -23,8 +23,17 @@ export const Image = function Image({
|
|||
}) {
|
||||
const { imageFormat, source, jsSchema, alternativeText, zoomButtons, rotateButton, loadingState, disabledState } =
|
||||
properties;
|
||||
const { imageFit, imageShape, backgroundColor, padding, customPadding, boxShadow, borderRadius, borderColor } =
|
||||
styles;
|
||||
const {
|
||||
imageFit,
|
||||
imageShape,
|
||||
backgroundColor,
|
||||
padding,
|
||||
customPadding,
|
||||
boxShadow,
|
||||
borderRadius,
|
||||
borderColor,
|
||||
alignment,
|
||||
} = styles;
|
||||
|
||||
const isInitialRender = useRef(true);
|
||||
|
||||
|
|
@ -168,6 +177,7 @@ export const Image = function Image({
|
|||
border: '1px solid',
|
||||
borderRadius: imageShape === 'circle' ? '50%' : `${borderRadius}px`,
|
||||
borderColor: borderColor ? borderColor : 'transparent',
|
||||
objectPosition: alignment,
|
||||
}}
|
||||
height={height}
|
||||
onClick={() => fireEvent('onClick')}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export const Pagination = ({
|
|||
width,
|
||||
}) => {
|
||||
const isInitialRender = useRef(true);
|
||||
const { visibility, disabledState, boxShadow } = styles;
|
||||
const { visibility, disabledState, boxShadow, alignment } = styles;
|
||||
const [currentPage, setCurrentPage] = useState(() => properties?.defaultPageIndex ?? 1);
|
||||
|
||||
const pageChanged = (number) => {
|
||||
|
|
@ -65,7 +65,12 @@ export const Pagination = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<div data-disabled={disabledState} className="d-flex align-items-center" data-cy={dataCy} style={{ boxShadow }}>
|
||||
<div
|
||||
data-disabled={disabledState}
|
||||
className="d-flex align-items-center"
|
||||
data-cy={dataCy}
|
||||
style={{ boxShadow, justifyContent: alignment }}
|
||||
>
|
||||
<ul className="pagination m-0" style={computedStyles}>
|
||||
<Pagination.Operator
|
||||
operator="<<"
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ import React from 'react';
|
|||
import DOMPurify from 'dompurify';
|
||||
|
||||
export const SvgImage = function Timeline({ properties, styles, height, dataCy }) {
|
||||
const { visibility, boxShadow } = styles;
|
||||
const { visibility, boxShadow, alignment } = styles;
|
||||
const { data } = properties;
|
||||
return (
|
||||
<div style={{ display: visibility ? '' : 'none', overflow: 'hidden', height: height, boxShadow }} data-cy={dataCy}>
|
||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(data) }}></div>
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: alignment }}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(data) }}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ import React from 'react';
|
|||
|
||||
export const Tags = function Tags({ width, height, properties, styles, dataCy }) {
|
||||
const { data } = properties;
|
||||
const { visibility, boxShadow } = styles;
|
||||
const { visibility, boxShadow, alignment } = styles;
|
||||
|
||||
const computedStyles = {
|
||||
width,
|
||||
height,
|
||||
display: visibility ? '' : 'none',
|
||||
display: visibility ? 'flex' : 'none',
|
||||
overflowY: 'auto',
|
||||
boxShadow,
|
||||
justifyContent: alignment,
|
||||
};
|
||||
|
||||
function renderTag(item, index) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ function Logs({ logProps, idx }) {
|
|||
logProps?.description ||
|
||||
(isString(logProps?.message) && logProps?.message) ||
|
||||
(isString(logProps?.error?.description) && logProps?.error?.description) || //added string check since description can be an object. eg: runpy
|
||||
logProps?.error?.message.trim()
|
||||
logProps?.error?.message
|
||||
}`;
|
||||
|
||||
const defaultStyles = {
|
||||
|
|
@ -87,12 +87,12 @@ function Logs({ logProps, idx }) {
|
|||
<p
|
||||
className="m-0 d-flex"
|
||||
onClick={(e) => {
|
||||
setOpen((prev) => !prev);
|
||||
logProps?.type !== 'Custom Log' && setOpen((prev) => !prev);
|
||||
}}
|
||||
style={{ pointerEvents: logProps?.isQuerySuccessLog ? 'none' : 'default', position: 'relative' }}
|
||||
>
|
||||
<span className={cx('position-absolute')} style={defaultStyles}>
|
||||
<SolidIcon name="rightarrrow" fill={`var(--icons-strong)`} width="16" />
|
||||
{logProps?.type !== 'Custom Log' && <SolidIcon name="rightarrrow" fill={`var(--icons-strong)`} width="16" />}
|
||||
</span>
|
||||
<span className="w-100" style={{ paddingTop: '8px', paddingBottom: '8px', paddingLeft: '20px' }}>
|
||||
{logProps.type === 'navToDisablePage' ? (
|
||||
|
|
@ -103,23 +103,32 @@ function Logs({ logProps, idx }) {
|
|||
<div className="error-target cursor-pointer">{logProps?.errorTarget}</div>
|
||||
<small className="text-slate-10 text-right ">{moment(logProps?.timestamp).fromNow()}</small>
|
||||
</div>
|
||||
<div className={`d-flex justify-content-between align-items-center ${!open && 'text-truncate'}`}>
|
||||
{logProps?.type === 'Custom Log' && (
|
||||
<div className="error-target-custom-log cursor-pointer">
|
||||
<SolidIcon name="code" fill={`var(--purple11)`} width="15" /> Custom Log
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`d-flex justify-content-between align-items-center ${
|
||||
!open && logProps?.type !== 'Custom Log' && 'text-truncate'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={` cursor-pointer debugger-error-title ${!open && 'text-truncate'} ${
|
||||
logProps?.errorTarget == 'Custom Log' && logProps?.logLevel == 'error' && 'text-tomato-9'
|
||||
}`}
|
||||
className={` cursor-pointer debugger-error-title font-weight-500 ${
|
||||
!open && logProps?.type !== 'Custom Log' && 'text-truncate'
|
||||
} ${logProps?.type == 'Custom Log' && logProps?.logLevel == 'error' && 'text-tomato-9'}`}
|
||||
>
|
||||
<HighlightSecondWord text={title} />
|
||||
</span>
|
||||
</div>
|
||||
{logProps?.type == 'Custom Log' && <div className="font-weight-500">{message}</div>}
|
||||
<span
|
||||
className={cx('mx-1', {
|
||||
className={cx('font-weight-500', {
|
||||
'text-tomato-9': !logProps?.isQuerySuccessLog,
|
||||
'color-light-green': logProps?.isQuerySuccessLog,
|
||||
})}
|
||||
>
|
||||
{message}
|
||||
{logProps?.error?.lineNumber ? `, Line ${logProps.error.lineNumber}` : ''}
|
||||
{logProps?.type !== 'Custom Log' && message}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ export const DateTimePicker = ({
|
|||
return (
|
||||
<div
|
||||
data-disabled={styles.disabledState}
|
||||
className={cx('datepicker-widget', {
|
||||
className={cx('datepicker-widget position-relative', {
|
||||
'theme-tjdb': !darkMode,
|
||||
'theme-dark': darkMode,
|
||||
})}
|
||||
|
|
@ -324,7 +324,7 @@ export const DateTimePicker = ({
|
|||
})}
|
||||
popperPlacement={'bottom-start'}
|
||||
popperClassName={cx({
|
||||
'tjdb-datepicker-reset': !isEditCell,
|
||||
// 'tjdb-datepicker-reset': !isEditCell,
|
||||
'tjdb-datepicker-celledit-reset': isEditCell,
|
||||
})}
|
||||
onInputClick={() => {
|
||||
|
|
|
|||
|
|
@ -117,6 +117,9 @@
|
|||
margin-top: 4px;
|
||||
box-shadow: 0px 8px 16px 0px #3032331A;
|
||||
}
|
||||
.react-datepicker-popper {
|
||||
z-index: 10001 !important;
|
||||
}
|
||||
|
||||
.react-datepicker-time__caption{
|
||||
margin-left:20px
|
||||
|
|
@ -234,4 +237,11 @@
|
|||
line-height: normal !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.table-schema-row{
|
||||
.react-datepicker-time__input-container{
|
||||
input{
|
||||
line-height: normal !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,6 +143,15 @@ export const imageConfig = {
|
|||
},
|
||||
accordian: 'Image',
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'center',
|
||||
},
|
||||
accordian: 'Image',
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Background',
|
||||
|
|
@ -255,6 +264,7 @@ export const imageConfig = {
|
|||
boxShadow: { value: '0px 0px 0px 0px #00000090' },
|
||||
padding: { value: 'default' },
|
||||
customPadding: { value: '{{0}}' },
|
||||
alignment: { value: 'center' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
|
|||
});
|
||||
}
|
||||
|
||||
if (item.kind === 'tooljetdb' && item.options.table_id) extractedIdData.push(item.options.table_id);
|
||||
if (item.kind === 'tooljetdb' && item.options.tableId) extractedIdData.push(item.options.tableId);
|
||||
});
|
||||
const uniqueSet = new Set(extractedIdData);
|
||||
const selectedVersiontable = Array.from(uniqueSet).map((item) => ({ table_id: item }));
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
libraryAppService,
|
||||
gitSyncService,
|
||||
licenseService,
|
||||
pluginsService,
|
||||
} from '@/_services';
|
||||
import { ConfirmDialog, AppModal } from '@/_components';
|
||||
import Select from '@/_ui/Select';
|
||||
|
|
@ -113,7 +114,7 @@ class HomePageComponent extends React.Component {
|
|||
showUserGroupMigrationModal: false,
|
||||
showGroupMigrationBanner: true,
|
||||
shouldAutoImportPlugin: false,
|
||||
dependentPluginsForTemplate: [],
|
||||
dependentPlugins: [],
|
||||
dependentPluginsDetail: {},
|
||||
};
|
||||
}
|
||||
|
|
@ -310,7 +311,7 @@ class HomePageComponent extends React.Component {
|
|||
const fileReader = new FileReader();
|
||||
const fileName = file.name.replace('.json', '').substring(0, 50);
|
||||
fileReader.readAsText(file, 'UTF-8');
|
||||
fileReader.onload = (event) => {
|
||||
fileReader.onload = async (event) => {
|
||||
const result = event.target.result;
|
||||
let fileContent;
|
||||
try {
|
||||
|
|
@ -319,8 +320,26 @@ class HomePageComponent extends React.Component {
|
|||
toast.error(`Could not import: ${parseError}`);
|
||||
return;
|
||||
}
|
||||
this.setState({ fileContent, fileName, showImportAppModal: true });
|
||||
|
||||
const importedAppDef = fileContent.app || fileContent.appV2;
|
||||
const dataSourcesUsedInApps = [];
|
||||
importedAppDef.forEach((appDefinition) => {
|
||||
appDefinition?.definition?.appV2?.dataSources.forEach((dataSource) => {
|
||||
dataSourcesUsedInApps.push(dataSource);
|
||||
});
|
||||
});
|
||||
|
||||
const dependentPluginsResponse = await pluginsService.findDependentPlugins(dataSourcesUsedInApps);
|
||||
const { pluginsToBeInstalled = [], pluginsListIdToDetailsMap = {} } = dependentPluginsResponse.data;
|
||||
this.setState({
|
||||
fileContent,
|
||||
fileName,
|
||||
showImportAppModal: true,
|
||||
dependentPlugins: pluginsToBeInstalled,
|
||||
dependentPluginsDetail: { ...pluginsListIdToDetailsMap },
|
||||
});
|
||||
};
|
||||
|
||||
fileReader.onerror = (error) => {
|
||||
toast.error(`Could not import the app: ${error}`);
|
||||
return;
|
||||
|
|
@ -348,12 +367,19 @@ class HomePageComponent extends React.Component {
|
|||
importJSON.app[0].appName = appName;
|
||||
}
|
||||
const requestBody = { organization_id, ...importJSON };
|
||||
let installedPluginsInfo = [];
|
||||
try {
|
||||
if (this.state.dependentPlugins.length) {
|
||||
({ installedPluginsInfo = [] } = await pluginsService.installDependentPlugins(
|
||||
this.state.dependentPlugins,
|
||||
true
|
||||
));
|
||||
}
|
||||
|
||||
const data = await appsService.importResource(requestBody);
|
||||
toast.success('App imported successfully.');
|
||||
this.setState({
|
||||
isImportingApp: false,
|
||||
});
|
||||
this.setState({ isImportingApp: false });
|
||||
|
||||
if (!isEmpty(data.imports.app)) {
|
||||
this.props.navigate(`/${getWorkspaceId()}/apps/${data.imports.app[0].id}`, {
|
||||
state: { commitEnabled: this.state.commitEnabled },
|
||||
|
|
@ -362,12 +388,13 @@ class HomePageComponent extends React.Component {
|
|||
this.props.navigate(`/${getWorkspaceId()}/database`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
isImportingApp: false,
|
||||
});
|
||||
if (error.statusCode === 409) {
|
||||
return false;
|
||||
if (installedPluginsInfo.length) {
|
||||
const pluginsId = installedPluginsInfo.map((pluginInfo) => pluginInfo.id);
|
||||
await pluginsService.uninstallPlugins(pluginsId);
|
||||
}
|
||||
|
||||
this.setState({ isImportingApp: false });
|
||||
if (error.statusCode === 409) return false;
|
||||
toast.error(error?.error || error?.message || 'App import failed');
|
||||
}
|
||||
};
|
||||
|
|
@ -380,7 +407,7 @@ class HomePageComponent extends React.Component {
|
|||
const data = await libraryAppService.deploy(
|
||||
id,
|
||||
appName,
|
||||
this.state.dependentPluginsForTemplate,
|
||||
this.state.dependentPlugins,
|
||||
this.state.shouldAutoImportPlugin
|
||||
);
|
||||
this.setState({ deploying: false });
|
||||
|
|
@ -732,7 +759,7 @@ class HomePageComponent extends React.Component {
|
|||
selectedTemplate: template,
|
||||
...(plugins_to_be_installed.length && {
|
||||
shouldAutoImportPlugin: true,
|
||||
dependentPluginsForTemplate: plugins_to_be_installed,
|
||||
dependentPlugins: plugins_to_be_installed,
|
||||
dependentPluginsDetail: { ...plugins_detail_by_id },
|
||||
}),
|
||||
});
|
||||
|
|
@ -750,7 +777,7 @@ class HomePageComponent extends React.Component {
|
|||
this.setState({
|
||||
showCreateAppFromTemplateModal: false,
|
||||
selectedTemplate: null,
|
||||
dependentPluginsForTemplate: [],
|
||||
dependentPlugins: [],
|
||||
dependentPluginsDetail: {},
|
||||
shouldAutoImportPlugin: false,
|
||||
});
|
||||
|
|
@ -763,6 +790,20 @@ class HomePageComponent extends React.Component {
|
|||
closeCreateAppModal = () => {
|
||||
this.setState({ showCreateAppModal: false, showCreateModuleModal: false });
|
||||
};
|
||||
|
||||
openImportAppModal = async () => {
|
||||
this.setState({ showImportAppModal: true });
|
||||
};
|
||||
|
||||
closeImportAppModal = () => {
|
||||
this.setState({
|
||||
showImportAppModal: false,
|
||||
dependentPlugins: [],
|
||||
dependentPluginsDetail: {},
|
||||
shouldAutoImportPlugin: false,
|
||||
});
|
||||
};
|
||||
|
||||
isWithinSevenDaysOfSignUp = (date) => {
|
||||
const currentDate = new Date().toISOString();
|
||||
const differenceInTime = new Date(currentDate).getTime() - new Date(date).getTime();
|
||||
|
|
@ -836,7 +877,7 @@ class HomePageComponent extends React.Component {
|
|||
workflowInstanceLevelLimit,
|
||||
showUserGroupMigrationModal,
|
||||
showGroupMigrationBanner,
|
||||
dependentPluginsForTemplate,
|
||||
dependentPlugins,
|
||||
dependentPluginsDetail,
|
||||
} = this.state;
|
||||
const modalConfigs = {
|
||||
|
|
@ -865,12 +906,14 @@ class HomePageComponent extends React.Component {
|
|||
modalType: 'import',
|
||||
closeModal: () => this.setState({ showImportAppModal: false }),
|
||||
processApp: this.importFile,
|
||||
show: () => this.setState({ showImportAppModal: true }),
|
||||
show: this.openImportAppModal,
|
||||
title: 'Import app',
|
||||
actionButton: 'Import app',
|
||||
actionLoadingButton: 'Importing',
|
||||
fileContent: fileContent,
|
||||
selectedAppName: fileName,
|
||||
dependentPluginsDetail: dependentPluginsDetail,
|
||||
dependentPlugins: dependentPlugins,
|
||||
},
|
||||
template: {
|
||||
modalType: 'template',
|
||||
|
|
@ -882,7 +925,7 @@ class HomePageComponent extends React.Component {
|
|||
actionLoadingButton: 'Creating',
|
||||
templateDetails: this.state.selectedTemplate,
|
||||
dependentPluginsDetail: dependentPluginsDetail,
|
||||
dependentPluginsForTemplate: dependentPluginsForTemplate,
|
||||
dependentPlugins: dependentPlugins,
|
||||
},
|
||||
};
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export function AppModal({
|
|||
handleCommitEnableChange,
|
||||
appType,
|
||||
dependentPluginsDetail = [],
|
||||
dependentPluginsForTemplate = [],
|
||||
dependentPlugins = [],
|
||||
}) {
|
||||
if (!selectedAppName && templateDetails) {
|
||||
selectedAppName = templateDetails?.name || '';
|
||||
|
|
@ -238,10 +238,10 @@ export function AppModal({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{dependentPluginsForTemplate && dependentPluginsForTemplate.length >= 1 && (
|
||||
{dependentPlugins && dependentPlugins.length >= 1 && (
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<PluginsListForAppModal
|
||||
dependentPluginsForTemplate={dependentPluginsForTemplate}
|
||||
dependentPlugins={dependentPlugins}
|
||||
dependentPluginsDetail={dependentPluginsDetail}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
510
frontend/src/_components/DynamicFormV2.jsx
Normal file
510
frontend/src/_components/DynamicFormV2.jsx
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import DataSourceSchemaManager from '@/_helpers/dataSourceSchemaManager';
|
||||
import Textarea from '@/_ui/Textarea';
|
||||
import Input from '@/_ui/Input';
|
||||
import Select from '@/_ui/Select';
|
||||
import Headers from '@/_ui/HttpHeaders';
|
||||
import Toggle from '@/_ui/Toggle';
|
||||
import InputV3 from '@/_ui/Input-V3';
|
||||
import { filter, find, isEmpty } from 'lodash';
|
||||
import { ButtonSolid } from './AppButton';
|
||||
import { useGlobalDataSourcesStatus } from '@/_stores/dataSourcesStore';
|
||||
import { canDeleteDataSource, canUpdateDataSource } from '@/_helpers';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import { orgEnvironmentVariableService, orgEnvironmentConstantService } from '../_services';
|
||||
import { Constants } from '@/_helpers/utils';
|
||||
|
||||
const DynamicFormV2 = ({
|
||||
schema,
|
||||
options,
|
||||
optionchanged,
|
||||
optionsChanged,
|
||||
selectedDataSource,
|
||||
isEditMode,
|
||||
layout = 'vertical',
|
||||
onBlur,
|
||||
setDefaultOptions,
|
||||
currentAppEnvironmentId,
|
||||
isGDS,
|
||||
validationMessages,
|
||||
setValidationMessages,
|
||||
clearValidationMessages,
|
||||
}) => {
|
||||
const uiProperties = schema['tj:ui:properties'] || {};
|
||||
const dsm = React.useMemo(() => new DataSourceSchemaManager(schema), [schema]);
|
||||
const encryptedProperties = React.useMemo(() => dsm.getEncryptedProperties(), [dsm]);
|
||||
const [conditionallyRequiredProperties, setConditionallyRequiredProperties] = React.useState([]);
|
||||
const [workspaceVariables, setWorkspaceVariables] = React.useState([]);
|
||||
const [currentOrgEnvironmentConstants, setCurrentOrgEnvironmentConstants] = React.useState([]);
|
||||
const [computedProps, setComputedProps] = React.useState({});
|
||||
const [hasUserInteracted, setHasUserInteracted] = React.useState(false);
|
||||
const [interactedFields, setInteractedFields] = React.useState(new Set());
|
||||
|
||||
const isHorizontalLayout = layout === 'horizontal';
|
||||
const prevDataSourceIdRef = React.useRef(selectedDataSource?.id);
|
||||
|
||||
const globalDataSourcesStatus = useGlobalDataSourcesStatus();
|
||||
const { isEditing: isDataSourceEditing } = globalDataSourcesStatus;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isGDS) {
|
||||
orgEnvironmentConstantService.getConstantsFromEnvironment(currentAppEnvironmentId).then((data) => {
|
||||
const constants = {
|
||||
globals: {},
|
||||
secrets: {},
|
||||
};
|
||||
data.constants.forEach((constant) => {
|
||||
if (constant.type === Constants.Secret) {
|
||||
constants.secrets[constant.name] = constant.value;
|
||||
} else {
|
||||
constants.globals[constant.name] = constant.value;
|
||||
}
|
||||
});
|
||||
|
||||
setCurrentOrgEnvironmentConstants(constants);
|
||||
});
|
||||
|
||||
orgEnvironmentVariableService.getVariables().then((data) => {
|
||||
const client_variables = {};
|
||||
const server_variables = {};
|
||||
data.variables.map((variable) => {
|
||||
if (variable.variable_type === 'server') {
|
||||
server_variables[variable.variable_name] = 'HiddenEnvironmentVariable';
|
||||
} else {
|
||||
client_variables[variable.variable_name] = variable.value;
|
||||
}
|
||||
});
|
||||
|
||||
setWorkspaceVariables({ client: client_variables, server: server_variables });
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
setWorkspaceVariables([]);
|
||||
setCurrentOrgEnvironmentConstants([]);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentAppEnvironmentId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!hasUserInteracted) return;
|
||||
const { valid, errors } = dsm.validateData(options);
|
||||
|
||||
if (valid) {
|
||||
clearValidationMessages();
|
||||
} else {
|
||||
setValidationMessages(errors, schema);
|
||||
const requiredFields = errors
|
||||
.filter((error) => error.keyword === 'required')
|
||||
.map((error) => error.params.missingProperty);
|
||||
setConditionallyRequiredProperties(requiredFields);
|
||||
}
|
||||
}, [options]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const prevDataSourceId = prevDataSourceIdRef.current;
|
||||
prevDataSourceIdRef.current = selectedDataSource?.id;
|
||||
const uiProperties = schema['tj:ui:properties'];
|
||||
if (!isEmpty(uiProperties)) {
|
||||
let fields = {};
|
||||
let encryptedFieldsProps = {};
|
||||
const flipComponentDropdown = find(uiProperties, ['widget', 'dropdown-component-flip']);
|
||||
|
||||
if (flipComponentDropdown) {
|
||||
const selector = options?.[flipComponentDropdown?.key]?.value;
|
||||
const commonFieldsFromSslCertificate = uiProperties[selector]?.ssl_certificate?.commonFields;
|
||||
fields = {
|
||||
...commonFieldsFromSslCertificate,
|
||||
...flipComponentDropdown?.commonFields,
|
||||
...uiProperties[selector],
|
||||
};
|
||||
} else {
|
||||
fields = { ...uiProperties };
|
||||
}
|
||||
|
||||
const processFields = (fieldsObject) => {
|
||||
Object.keys(fieldsObject).forEach((key) => {
|
||||
const field = fieldsObject[key];
|
||||
const { widget, encrypted, key: propertyKey } = field;
|
||||
|
||||
if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) {
|
||||
encryptedFieldsProps[propertyKey] = {
|
||||
disabled: !!selectedDataSource?.id,
|
||||
};
|
||||
} else if (!isDataSourceEditing) {
|
||||
if (widget === 'password' || encrypted) {
|
||||
encryptedFieldsProps[propertyKey] = {
|
||||
disabled: true,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if ((widget === 'password' || encrypted) && !(propertyKey in computedProps)) {
|
||||
encryptedFieldsProps[propertyKey] = {
|
||||
disabled: !!selectedDataSource?.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// To check for nested dropdown-component-flip
|
||||
if (widget === 'dropdown-component-flip') {
|
||||
const selectedOption = options?.[field.key]?.value;
|
||||
|
||||
if (field.commonFields) {
|
||||
processFields(field.commonFields);
|
||||
}
|
||||
|
||||
if (selectedOption && fieldsObject[selectedOption]) {
|
||||
processFields(fieldsObject[selectedOption]);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
processFields(fields);
|
||||
|
||||
if (uiProperties.renderForm) {
|
||||
Object.keys(uiProperties.renderForm).forEach((sectionKey) => {
|
||||
const section = uiProperties.renderForm[sectionKey];
|
||||
const { inputs } = section;
|
||||
if (inputs) {
|
||||
processFields(inputs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (prevDataSourceId !== selectedDataSource?.id) {
|
||||
setComputedProps({ ...encryptedFieldsProps });
|
||||
} else {
|
||||
setComputedProps({ ...computedProps, ...encryptedFieldsProps });
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedDataSource?.id, options, isDataSourceEditing]);
|
||||
|
||||
const getElement = (type) => {
|
||||
switch (type) {
|
||||
case 'password':
|
||||
case 'text':
|
||||
return Input;
|
||||
case 'password-v3':
|
||||
case 'text-v3':
|
||||
return InputV3;
|
||||
case 'textarea':
|
||||
return Textarea;
|
||||
case 'toggle':
|
||||
return Toggle;
|
||||
case 'react-component-headers':
|
||||
return Headers;
|
||||
// TODO: Move dropdown component flip logic to be handled here
|
||||
// case 'dropdown-component-flip':
|
||||
// return Select;
|
||||
default:
|
||||
return <div>Type is invalid</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const getElementProps = (uiProperties) => {
|
||||
const { label, description, widget, required, width, key, help_text: helpText, list, buttonText } = uiProperties;
|
||||
|
||||
const isRequired = required || conditionallyRequiredProperties.includes(key);
|
||||
const isEncrypted = widget === 'password-v3' || encryptedProperties.includes(key);
|
||||
const currentValue = options?.[key]?.value;
|
||||
|
||||
const handleOptionChange = (key, value, flag) => {
|
||||
if (!hasUserInteracted) {
|
||||
setHasUserInteracted(true);
|
||||
}
|
||||
setInteractedFields((prev) => new Set(prev).add(key));
|
||||
optionchanged(key, value, flag);
|
||||
};
|
||||
|
||||
switch (widget) {
|
||||
case 'password':
|
||||
case 'text':
|
||||
case 'textarea': {
|
||||
return {
|
||||
key,
|
||||
widget,
|
||||
label,
|
||||
placeholder: isEncrypted ? '**************' : description,
|
||||
className: cx('form-control', {
|
||||
'dynamic-form-encrypted-field': isEncrypted,
|
||||
}),
|
||||
style: { marginBottom: '0px !important' },
|
||||
helpText: helpText,
|
||||
value: currentValue || '',
|
||||
onChange: (e) => optionchanged(key, e.target.value, true),
|
||||
isGDS: true,
|
||||
workspaceVariables: [],
|
||||
workspaceConstants: [],
|
||||
encrypted: isEncrypted,
|
||||
onBlur,
|
||||
};
|
||||
}
|
||||
case 'password-v3':
|
||||
case 'text-v3': {
|
||||
return {
|
||||
key,
|
||||
widget,
|
||||
label,
|
||||
placeholder: isEncrypted ? '**************' : description,
|
||||
className: cx('form-control', {
|
||||
'dynamic-form-encrypted-field': isEncrypted,
|
||||
}),
|
||||
style: { marginBottom: '0px !important' },
|
||||
helpText: helpText,
|
||||
value: currentValue || '',
|
||||
onChange: (e) => handleOptionChange(key, e.target.value, true),
|
||||
isGDS: true,
|
||||
workspaceVariables: [],
|
||||
workspaceConstants: [],
|
||||
encrypted: isEncrypted,
|
||||
onBlur,
|
||||
isRequired: isRequired,
|
||||
isValidatedMessages:
|
||||
!hasUserInteracted || !interactedFields.has(key)
|
||||
? { valid: null, message: '' } // skip validation for initial render and untouched elements
|
||||
: validationMessages[key]
|
||||
? { valid: false, message: validationMessages[key] }
|
||||
: isRequired && !isEncrypted
|
||||
? { valid: true, message: '' }
|
||||
: { valid: null, message: '' }, // handle optional && encrypted fields
|
||||
isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(),
|
||||
};
|
||||
}
|
||||
case 'react-component-headers': {
|
||||
let isRenderedAsQueryEditor;
|
||||
if (isGDS) {
|
||||
isRenderedAsQueryEditor = false;
|
||||
} else {
|
||||
isRenderedAsQueryEditor = !isGDS;
|
||||
}
|
||||
return {
|
||||
getter: key,
|
||||
options: isRenderedAsQueryEditor
|
||||
? options?.[key] ?? schema?.defaults?.[key]
|
||||
: options?.[key]?.value ?? schema?.defaults?.[key]?.value,
|
||||
optionchanged,
|
||||
isRenderedAsQueryEditor,
|
||||
workspaceConstants: currentOrgEnvironmentConstants,
|
||||
isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(),
|
||||
encrypted: isEncrypted,
|
||||
buttonText,
|
||||
width: width,
|
||||
};
|
||||
}
|
||||
case 'toggle':
|
||||
return {
|
||||
defaultChecked: currentValue,
|
||||
checked: currentValue,
|
||||
onChange: (e) => optionchanged(key, e.target.checked),
|
||||
};
|
||||
case 'dropdown':
|
||||
case 'dropdown-component-flip':
|
||||
return {
|
||||
options: list,
|
||||
value: options?.[key]?.value || options?.[key],
|
||||
onChange: (value) => optionchanged(key, value),
|
||||
width: width || '100%',
|
||||
encrypted: options?.[key]?.encrypted,
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const getLayout = (uiProperties) => {
|
||||
if (isEmpty(uiProperties)) return null;
|
||||
const flipComponentDropdown = isFlipComponentDropdown(uiProperties);
|
||||
|
||||
if (flipComponentDropdown) {
|
||||
return flipComponentDropdown;
|
||||
}
|
||||
|
||||
const handleEncryptedFieldsToggle = (event, field) => {
|
||||
if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) {
|
||||
return;
|
||||
}
|
||||
const isEditing = computedProps[field]['disabled'];
|
||||
if (isEditing) {
|
||||
optionchanged(field, '');
|
||||
} else {
|
||||
//Send old field value if editing mode disabled for encrypted fields
|
||||
const newOptions = { ...options };
|
||||
const oldFieldValue = selectedDataSource?.['options']?.[field];
|
||||
if (oldFieldValue) {
|
||||
optionsChanged({ ...newOptions, [field]: oldFieldValue });
|
||||
} else {
|
||||
delete newOptions[field];
|
||||
optionsChanged({ ...newOptions });
|
||||
}
|
||||
}
|
||||
setComputedProps({
|
||||
...computedProps,
|
||||
[field]: {
|
||||
...computedProps[field],
|
||||
disabled: !isEditing,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderLabel = (label, tooltip) => {
|
||||
const labelElement = (
|
||||
<label
|
||||
className="form-label"
|
||||
data-cy={`label-${String(label).toLowerCase().replace(/\s+/g, '-')}`}
|
||||
style={{ textDecoration: tooltip ? 'underline 2px dashed' : 'none', textDecorationColor: 'var(--slate8)' }}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
trigger="click"
|
||||
rootClose
|
||||
overlay={<Tooltip id={`tooltip-${label}`}>{tooltip}</Tooltip>}
|
||||
>
|
||||
{labelElement}
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
return labelElement;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${isHorizontalLayout ? '' : 'row'}`}>
|
||||
{Object.keys(uiProperties).map((key) => {
|
||||
const { label, widget, encrypted, className, key: propertyKey } = uiProperties[key];
|
||||
const Element = getElement(widget);
|
||||
const isSpecificComponent = ['tooljetdb-operations', 'react-component-api-endpoint'].includes(widget);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('my-2', {
|
||||
'col-md-12': !className && !isHorizontalLayout,
|
||||
[className]: !!className,
|
||||
'd-flex': isHorizontalLayout,
|
||||
'dynamic-form-row': isHorizontalLayout,
|
||||
})}
|
||||
key={key}
|
||||
>
|
||||
{!isSpecificComponent && (
|
||||
<div
|
||||
className={cx('d-flex', {
|
||||
'form-label': isHorizontalLayout,
|
||||
'align-items-center': !isHorizontalLayout,
|
||||
})}
|
||||
style={{ minWidth: '100px' }}
|
||||
>
|
||||
{label &&
|
||||
widget !== 'text-v3' &&
|
||||
widget !== 'password-v3' &&
|
||||
renderLabel(label, uiProperties[key].tooltip)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cx(
|
||||
{
|
||||
'flex-grow-1': isHorizontalLayout && !isSpecificComponent,
|
||||
'w-100': isHorizontalLayout && widget !== 'codehinter',
|
||||
},
|
||||
'dynamic-form-element'
|
||||
)}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Element
|
||||
{...getElementProps(uiProperties[key])}
|
||||
{...computedProps[propertyKey]}
|
||||
data-cy={`${String(label).toLocaleLowerCase().replace(/\s+/g, '-')}-text-field`}
|
||||
//to be removed after whole ui is same
|
||||
isHorizontalLayout={isHorizontalLayout}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FlipComponentDropdown = (uiProperties) => {
|
||||
const flipComponentDropdowns = filter(uiProperties, ['widget', 'dropdown-component-flip']);
|
||||
|
||||
const dropdownComponents = flipComponentDropdowns.map((flipComponentDropdown) => {
|
||||
const selector = options?.[flipComponentDropdown?.key]?.value || options?.[flipComponentDropdown?.key];
|
||||
|
||||
return (
|
||||
<div key={flipComponentDropdown.key}>
|
||||
<div className={isHorizontalLayout ? '' : 'row'}>
|
||||
{flipComponentDropdown.commonFields && getLayout(flipComponentDropdown.commonFields)}
|
||||
|
||||
<div
|
||||
className={cx('my-2', {
|
||||
'col-md-12': !flipComponentDropdown.className && !isHorizontalLayout,
|
||||
'd-flex': isHorizontalLayout,
|
||||
'dynamic-form-row': isHorizontalLayout,
|
||||
[flipComponentDropdown.className]: !!flipComponentDropdown.className,
|
||||
})}
|
||||
>
|
||||
{(flipComponentDropdown.label || isHorizontalLayout) && (
|
||||
<label
|
||||
className={cx('form-label')}
|
||||
data-cy={`${String(flipComponentDropdown.label)
|
||||
.toLocaleLowerCase()
|
||||
.replace(/\s+/g, '-')}-dropdown-label`}
|
||||
>
|
||||
{flipComponentDropdown.label}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div data-cy={'query-select-dropdown'} className={cx({ 'flex-grow-1': isHorizontalLayout })}>
|
||||
<Select {...getElementProps(flipComponentDropdown)} styles={{}} useCustomStyles={false} />
|
||||
</div>
|
||||
{flipComponentDropdown.helpText && (
|
||||
<span className="flip-dropdown-help-text">{flipComponentDropdown.helpText}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{getLayout(uiProperties[selector])}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const normalComponents = Object.keys(uiProperties).map((key) => {
|
||||
const component = uiProperties[key];
|
||||
|
||||
if (component.type && component.type !== 'dropdown-component-flip') {
|
||||
return <div key={key}>{getLayout({ [key]: component })}</div>;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{normalComponents}
|
||||
{dropdownComponents}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const isFlipComponentDropdown = (uiProperties) => {
|
||||
const checkFlipComponents = filter(uiProperties, ['widget', 'dropdown-component-flip']);
|
||||
if (checkFlipComponents.length > 0) {
|
||||
return FlipComponentDropdown(uiProperties);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const flipComponentDropdown = isFlipComponentDropdown(uiProperties);
|
||||
if (flipComponentDropdown) return flipComponentDropdown;
|
||||
return getLayout(uiProperties);
|
||||
};
|
||||
|
||||
export default DynamicFormV2;
|
||||
|
|
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import config from 'config';
|
||||
|
||||
export const PluginsListForAppModal = ({ dependentPluginsForTemplate, dependentPluginsDetail }) => {
|
||||
export const PluginsListForAppModal = ({ dependentPlugins, dependentPluginsDetail }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const toggleExpanded = () => {
|
||||
|
|
@ -29,7 +29,7 @@ export const PluginsListForAppModal = ({ dependentPluginsForTemplate, dependentP
|
|||
)}
|
||||
</span>
|
||||
|
||||
{isExpanded && dependentPluginsForTemplate && dependentPluginsForTemplate.length > 0 && (
|
||||
{isExpanded && dependentPlugins && dependentPlugins.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
minHeight: '20px',
|
||||
|
|
@ -38,7 +38,7 @@ export const PluginsListForAppModal = ({ dependentPluginsForTemplate, dependentP
|
|||
borderLeft: '1px solid var(--border-weak)',
|
||||
}}
|
||||
>
|
||||
{dependentPluginsForTemplate.map((plugin, index) => {
|
||||
{dependentPlugins.map((plugin, index) => {
|
||||
const pluginsName = dependentPluginsDetail[plugin].name || plugin;
|
||||
const iconSrc = `${config.TOOLJET_MARKETPLACE_URL}/marketplace-assets/${plugin}/lib/icon.svg`;
|
||||
return (
|
||||
|
|
|
|||
119
frontend/src/_helpers/dataSourceSchemaManager.js
Normal file
119
frontend/src/_helpers/dataSourceSchemaManager.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import Ajv2020 from 'ajv';
|
||||
|
||||
const ajvOptions = {
|
||||
strict: false,
|
||||
allErrors: true,
|
||||
removeAdditional: false,
|
||||
// We disable meta-schema validation to avoid the schema being validated
|
||||
// against the official 2020-12 standard in ways that conflict with custom keywords.
|
||||
validateSchema: false,
|
||||
coerceTypes: true,
|
||||
errorDataPath: 'property',
|
||||
};
|
||||
|
||||
export default class DataSourceSchemaManager {
|
||||
constructor(schema) {
|
||||
this.schema = schema;
|
||||
this.ajv = new Ajv2020(ajvOptions);
|
||||
this.validate = this.ajv.compile(this.schema);
|
||||
}
|
||||
|
||||
validateData(options) {
|
||||
const data = this._convertDataSourceOptionsToData(options);
|
||||
const valid = this.validate(data);
|
||||
if (!valid) {
|
||||
return { valid: false, errors: this.validate.errors };
|
||||
}
|
||||
return { valid: true, errors: [] };
|
||||
}
|
||||
|
||||
getDefaults(options = {}) {
|
||||
const dataWithDefaults = { ...this._convertDataSourceOptionsToData(options) };
|
||||
|
||||
// AJV does not support defaults with conditional schemas
|
||||
// https://ajv.js.org/guide/modifying-data.html#assigning-defaults
|
||||
// Create a schema without conditional properties for default value assignment
|
||||
const schemaWithoutConditionals = {
|
||||
type: this.schema.type,
|
||||
properties: { ...this.schema.properties },
|
||||
};
|
||||
|
||||
// Compile the schema without conditionals to fill in default values
|
||||
const ajvForDefaults = new Ajv2020({
|
||||
...ajvOptions,
|
||||
useDefaults: true,
|
||||
});
|
||||
const applyDefaults = ajvForDefaults.compile(schemaWithoutConditionals);
|
||||
applyDefaults(dataWithDefaults);
|
||||
|
||||
const encryptedProperties = this.getEncryptedProperties();
|
||||
|
||||
// Combine the data with defaults and set encrypted fields to null
|
||||
const combinedData = {
|
||||
...dataWithDefaults,
|
||||
...Object.fromEntries(encryptedProperties.map((key) => [key, null])),
|
||||
};
|
||||
|
||||
return Object.entries(combinedData).reduce((result, [key, value]) => {
|
||||
result[key] = {
|
||||
value: value,
|
||||
encrypted: encryptedProperties.includes(key),
|
||||
};
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
getEncryptedProperties() {
|
||||
return this.schema['tj:encrypted'] || [];
|
||||
}
|
||||
|
||||
getSourceMetadata() {
|
||||
const { name, kind, type } = this.schema['tj:source'];
|
||||
|
||||
if (!name || !kind || !type) {
|
||||
throw new Error('Schema is missing required source metadata');
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
kind,
|
||||
type,
|
||||
options: this._getOptionsMetadata(),
|
||||
// Can remove exposed variables?
|
||||
exposedVariables: {
|
||||
isLoading: false,
|
||||
data: {},
|
||||
rawData: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_convertDataSourceOptionsToData(options) {
|
||||
return Object.entries(options).reduce((result, [key, { value }]) => {
|
||||
// Skip empty string values
|
||||
if (value !== '' && value !== null && value !== undefined) {
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
// Add a dummy value to pass validation for encrypted keys
|
||||
if (this.getEncryptedProperties().includes(key)) {
|
||||
result[key] = 'REDACTED';
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
_getOptionsMetadata() {
|
||||
const options = {};
|
||||
const properties = this.schema.properties || {};
|
||||
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
options[key] = { type: value.type };
|
||||
if (value.encrypted) {
|
||||
options[key].encrypted = true;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,11 +8,11 @@ export const libraryAppService = {
|
|||
findDependentPluginsInTemplate,
|
||||
};
|
||||
|
||||
function deploy(identifier, appName, dependentPluginsForTemplate = [], shouldAutoImportPlugin = false) {
|
||||
function deploy(identifier, appName, dependentPlugins = [], shouldAutoImportPlugin = false) {
|
||||
const body = {
|
||||
identifier,
|
||||
appName,
|
||||
dependentPluginsForTemplate,
|
||||
dependentPlugins,
|
||||
shouldAutoImportPlugin,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import HttpClient from '@/_helpers/http-client';
|
||||
import config from 'config';
|
||||
import { authHeader, handleResponse } from '@/_helpers';
|
||||
|
||||
const adapter = new HttpClient();
|
||||
|
||||
|
|
@ -22,10 +24,47 @@ function reloadPlugin(id) {
|
|||
return adapter.post(`/plugins/${id}/reload`);
|
||||
}
|
||||
|
||||
function findDependentPlugins(dataSources) {
|
||||
return adapter.post(`/plugins/findDependentPlugins`, dataSources);
|
||||
}
|
||||
|
||||
function installDependentPlugins(dependentPlugins, shouldAutoImportPlugin) {
|
||||
const body = {
|
||||
dependentPlugins,
|
||||
shouldAutoImportPlugin,
|
||||
};
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/plugins/installDependentPlugins`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function uninstallPlugins(pluginsId) {
|
||||
const body = {
|
||||
pluginsId: pluginsId,
|
||||
};
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
|
||||
return fetch(`${config.apiUrl}/plugins/uninstallPlugins`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
export const pluginsService = {
|
||||
findAll,
|
||||
installPlugin,
|
||||
updatePlugin,
|
||||
deletePlugin,
|
||||
reloadPlugin,
|
||||
findDependentPlugins,
|
||||
installDependentPlugins,
|
||||
uninstallPlugins,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@
|
|||
|
||||
.text-tomato-9 {
|
||||
color: var(--tomato9);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-slate-10 {
|
||||
|
|
@ -252,12 +253,25 @@
|
|||
background-color: var(--slate3);
|
||||
}
|
||||
|
||||
.error-target {
|
||||
.error-target, .error-target-custom-log {
|
||||
background-color: var(--interactive-overlays-fill-hover) !important;
|
||||
padding: 4px 7px;
|
||||
border-radius: 7px;
|
||||
color: var(--slate10)
|
||||
}
|
||||
|
||||
.error-target-custom-log {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
background: var(--purple5) !important;
|
||||
color: var(--purple11);
|
||||
width: fit-content;
|
||||
|
||||
svg {
|
||||
margin-right: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export default ({
|
|||
disabled={isDisabled}
|
||||
style={{
|
||||
flex: 1,
|
||||
width: width ? width : '300px',
|
||||
width: '316px',
|
||||
borderTopRightRadius: '0',
|
||||
borderBottomRightRadius: '0',
|
||||
borderRight: 'none',
|
||||
|
|
|
|||
21
frontend/src/_ui/Icon/solidIcons/Code.jsx
Normal file
21
frontend/src/_ui/Icon/solidIcons/Code.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
const Code = ({ fill = 'var(--purple11)', width = '25', className = '', viewBox = '0 0 25 25' }) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={width}
|
||||
viewBox={viewBox}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.05619 2.62763C4.45804 2.22576 5.0031 2 5.57142 2H14.1428C14.3323 2 14.514 2.07525 14.6479 2.20921L20.3623 7.9235C20.4961 8.05744 20.5714 8.23913 20.5714 8.42857V19.8571C20.5714 20.4254 20.3457 20.9706 19.9438 21.3724C19.542 21.7743 18.9968 22 18.4286 22H5.57142C5.00309 22 4.45804 21.7743 4.05619 21.3724C3.65433 20.9706 3.42856 20.4254 3.42856 19.8571V4.14286C3.42856 3.57454 3.65433 3.02949 4.05619 2.62763ZM10.6147 10.5281C11.0332 10.9465 11.0332 11.6249 10.6147 12.0433L8.51521 14.1429L10.6147 16.2424C11.0332 16.6609 11.0332 17.3391 10.6147 17.7576C10.1963 18.176 9.51793 18.176 9.09951 17.7576L6.24237 14.9005C5.82396 14.4821 5.82396 13.8037 6.24237 13.3852L9.09951 10.5281C9.51793 10.1097 10.1963 10.1097 10.6147 10.5281ZM13.3852 12.0433C12.9668 11.6249 12.9668 10.9465 13.3852 10.5281C13.8036 10.1097 14.482 10.1097 14.9005 10.5281L17.7576 13.3852C18.176 13.8037 18.176 14.4821 17.7576 14.9005L14.9005 17.7576C14.482 18.176 13.8036 18.176 13.3852 17.7576C12.9668 17.3391 12.9668 16.6609 13.3852 16.2424L15.4848 14.1429L13.3852 12.0433Z"
|
||||
fill={fill}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default Code;
|
||||
|
|
@ -229,6 +229,7 @@ import CalendarSmall from './CalendarSmall.jsx';
|
|||
import UserGroupsGrey from './UserGroupsGrey.jsx';
|
||||
import AppLimitSvg from './AppLimitSvg.jsx';
|
||||
import NewTabSmall from './NewTabSmall.jsx';
|
||||
import Code from './Code.jsx';
|
||||
|
||||
const Icon = (props) => {
|
||||
switch (props.name) {
|
||||
|
|
@ -308,6 +309,8 @@ const Icon = (props) => {
|
|||
return <CircularToggleEnabled {...props} />;
|
||||
case 'clearrectangle':
|
||||
return <ClearRectangle {...props} />;
|
||||
case 'code':
|
||||
return <Code {...props} />;
|
||||
case 'clock':
|
||||
return <Clock {...props} />;
|
||||
case 'column':
|
||||
|
|
|
|||
85
frontend/src/_ui/Input-V3/index.js
Normal file
85
frontend/src/_ui/Input-V3/index.js
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import React, { useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import OrgConstantVariablesPreviewBox from '../../_components/OrgConstantsVariablesResolver';
|
||||
import SolidIcon from '../Icon/SolidIcons';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import InputComponent from '@/components/ui/Input/Index';
|
||||
|
||||
const InputV3 = ({ helpText, ...props }) => {
|
||||
const { workspaceVariables, workspaceConstants, value, widget, disabled, encrypted } = props;
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const handleCopyToClipboard = async () => {
|
||||
if (widget === 'copyToClipboard') {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
toast.success('Copied to clipboard');
|
||||
setIsCopied(true);
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 4000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tj-app-input">
|
||||
<div
|
||||
className={cx('', {
|
||||
'tj-app-input-wrapper': widget === 'password' || widget === 'copyToClipboard' || encrypted,
|
||||
})}
|
||||
style={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
{widget === 'text-v3' && (
|
||||
<InputComponent
|
||||
{...props}
|
||||
value={value}
|
||||
styles="tw-bg-transparent"
|
||||
label={props.label}
|
||||
placeholder={props.placeholder}
|
||||
required={props.isRequired}
|
||||
/>
|
||||
)}
|
||||
{(widget === 'password-v3' || encrypted) && (
|
||||
<div style={{ flex: '1' }}>
|
||||
<InputComponent
|
||||
{...props}
|
||||
type="password"
|
||||
value={value}
|
||||
styles="tw-bg-transparent"
|
||||
label={props.label}
|
||||
placeholder={props.placeholder}
|
||||
required={props.isRequired}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{widget === 'copyToClipboard' &&
|
||||
value &&
|
||||
(!isCopied ? (
|
||||
<div style={{ cursor: 'pointer' }} onClick={handleCopyToClipboard}>
|
||||
{' '}
|
||||
<SolidIcon className="copy-icon" name="copy" />
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ color: 'green' }}>
|
||||
{' '}
|
||||
<span>Copied!</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<OrgConstantVariablesPreviewBox
|
||||
workspaceVariables={workspaceVariables}
|
||||
workspaceConstants={workspaceConstants}
|
||||
isFocused={isFocused}
|
||||
value={value}
|
||||
/>
|
||||
{helpText && <small className="text-muted" dangerouslySetInnerHTML={{ __html: helpText }} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputV3;
|
||||
|
|
@ -1,28 +1,79 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import NumberInput from './NumberInput';
|
||||
import TextInput from './TextInput';
|
||||
import { HelperMessage, InputLabel, ValidationMessage } from '../InputUtils/InputUtils';
|
||||
import { ButtonSolid } from '../../../../_components/AppButton';
|
||||
|
||||
const CommonInput = ({ label, helperText, disabled, required, ...restProps }) => {
|
||||
const InputComponentType = restProps.type === 'number' ? NumberInput : TextInput;
|
||||
const CommonInput = ({ label, helperText, disabled, required, onChange: change, ...restProps }) => {
|
||||
const { type, encrypted, validation, isValidatedMessages, isDisabled } = restProps;
|
||||
|
||||
const InputComponentType = type === 'number' ? NumberInput : TextInput;
|
||||
const [isValid, setIsValid] = useState(null);
|
||||
const [message, setMessage] = useState('');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const isEncrypted = type === 'password' || encrypted;
|
||||
|
||||
const handleChange = (e) => {
|
||||
let validateObj;
|
||||
if (restProps.validation) {
|
||||
validateObj = restProps.validation(e);
|
||||
if (validation) {
|
||||
const validateObj = validation(e);
|
||||
setIsValid(validateObj.valid);
|
||||
setMessage(validateObj.message);
|
||||
change(e, validateObj);
|
||||
} else {
|
||||
change(e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isValidatedMessages) {
|
||||
setIsValid(isValidatedMessages.valid);
|
||||
setMessage(isValidatedMessages.message);
|
||||
}
|
||||
}, [isValidatedMessages]);
|
||||
|
||||
const toggleEditing = () => {
|
||||
if (isDisabled) return;
|
||||
|
||||
const willBeInEditMode = !isEditing;
|
||||
setIsEditing(willBeInEditMode);
|
||||
|
||||
if (willBeInEditMode) {
|
||||
change({ target: { value: '' } });
|
||||
}
|
||||
restProps.onChange(e, validateObj);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{label && <InputLabel disabled={disabled} label={label} required={required} />}
|
||||
<div className="d-flex">
|
||||
{label && <InputLabel disabled={disabled} label={label} required={required} />}
|
||||
{type === 'password' && (
|
||||
<div className="d-flex justify-content-between w-100">
|
||||
<div className="mx-1 col">
|
||||
<ButtonSolid
|
||||
className="datasource-edit-btn mb-2"
|
||||
type="a"
|
||||
variant="tertiary"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
disabled={isDisabled}
|
||||
onClick={toggleEditing}
|
||||
>
|
||||
{isEditing ? 'Cancel' : 'Edit'}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
|
||||
<div className="col-auto mb-2">
|
||||
<small className="text-green">
|
||||
<img className="mx-2 encrypted-icon" src="assets/images/icons/padlock.svg" width="12" height="12" />
|
||||
Encrypted
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<InputComponentType
|
||||
disabled={disabled}
|
||||
disabled={disabled || (isEncrypted && !isEditing)}
|
||||
required={required}
|
||||
response={isValid}
|
||||
onChange={handleChange}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ const NumberInput = ({ size, leadingIcon, response, disabled, ...restProps }) =>
|
|||
const inputStyle = `tw-border-border-default placeholder:tw-text-text-placeholder tw-font-normal disabled:tw-bg-[#CCD1D5]/30 tw-pr-[12px] ${
|
||||
leadingIcon ? (size === 'small' ? 'tw-pl-[32px]' : 'tw-pl-[34px]') : 'tw-pl-[12px]'
|
||||
} ${
|
||||
response === true ? 'tw-border-border-success-strong' : response === false ? 'tw-border-border-danger-strong' : ''
|
||||
response === true
|
||||
? 'tw-border-border-success-strong focus-visible:!tw-ring-0 focus-visible:!tw-ring-offset-0 focus-visible:!tw-border-border-success-strong'
|
||||
: response === false
|
||||
? 'tw-border-border-danger-strong focus-visible:!tw-ring-0 focus-visible:!tw-ring-offset-0 focus-visible:!tw-border-border-success-strong'
|
||||
: ''
|
||||
}`;
|
||||
|
||||
const handleIncrement = () => {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@ const TextInput = ({
|
|||
const inputStyle = `tw-border-border-default placeholder:tw-text-text-placeholder tw-font-normal disabled:tw-bg-[#CCD1D5]/30 ${
|
||||
leadingIcon ? (size === 'small' ? 'tw-pl-[32px]' : 'tw-pl-[34px]') : 'tw-pl-[12px]'
|
||||
} ${trailingAction ? (size === 'small' ? 'tw-pr-[40px]' : 'tw-pr-[44px]') : 'tw-pr-[12px]'} ${
|
||||
response === true ? '!tw-border-border-success-strong' : response === false ? '!tw-border-border-danger-strong' : ''
|
||||
response === true
|
||||
? '!tw-border-border-success-strong focus-visible:!tw-ring-0 focus-visible:!tw-ring-offset-0 focus-visible:!tw-border-border-success-strong'
|
||||
: response === false
|
||||
? '!tw-border-border-danger-strong focus-visible:!tw-ring-0 focus-visible:!tw-ring-offset-0 focus-visible:!tw-border-border-danger-strong'
|
||||
: ''
|
||||
}`;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ InputComponent.defaultProps = {
|
|||
size: 'medium',
|
||||
disabled: false,
|
||||
readOnly: '',
|
||||
validation: (e) => {},
|
||||
validation: null,
|
||||
label: '',
|
||||
'aria-label': '',
|
||||
required: false,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,40 @@
|
|||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { inputVariants } from './InputUtils/Variants';
|
||||
import SolidIcon from '../../../_ui/Icon/SolidIcons';
|
||||
|
||||
const Input = React.forwardRef(({ className, size, type, ...props }, ref) => {
|
||||
const [isPasswordVisible, setIsPasswordVisible] = React.useState(false);
|
||||
const isPasswordField = type === 'password';
|
||||
|
||||
const togglePasswordVisibility = () => {
|
||||
if (!props.disabled) {
|
||||
setIsPasswordVisible((prev) => !prev);
|
||||
}
|
||||
};
|
||||
|
||||
const Input = React.forwardRef(({ className, size, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
inputVariants({ size }),
|
||||
`tw-peer tw-flex tw-text-[12px]/[18px] tw-w-full tw-rounded-[8px] tw-border-[1px] tw-border-solid tw-bg-background-surface-layer-01 tw-py-[7px] tw-text-text-default focus-visible:tw-ring-[1px] focus-visible:tw-ring-offset-[1px] focus-visible:tw-ring-border-accent-strong focus-visible:tw-ring-offset-border-accent-strong focus-visible:tw-border-transparent disabled:tw-cursor-not-allowed disabled:tw-border-transparent`,
|
||||
className
|
||||
<>
|
||||
<input
|
||||
type={isPasswordField && isPasswordVisible ? 'text' : type}
|
||||
className={cn(
|
||||
inputVariants({ size }),
|
||||
`tw-relative tw-peer tw-flex tw-text-[12px]/[18px] tw-w-full tw-rounded-[8px] tw-border-[1px] tw-border-solid tw-bg-background-surface-layer-01 tw-py-[7px] tw-text-text-default focus-visible:tw-ring-[1px] focus-visible:tw-ring-offset-[1px] focus-visible:tw-ring-border-accent-strong focus-visible:tw-ring-offset-border-accent-strong focus-visible:tw-border-transparent disabled:tw-cursor-not-allowed ${props.styles}`,
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
{isPasswordField && (
|
||||
<div onClick={togglePasswordVisibility}>
|
||||
{isPasswordVisible ? (
|
||||
<SolidIcon className="eye-icon" name="eye" />
|
||||
) : (
|
||||
<SolidIcon className="eye-icon" name="eyedisable" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
Input.displayName = 'Input';
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const ValidationMessage = ({ response, validationMessage, className }) =>
|
|||
htmlFor="validation"
|
||||
type="helper"
|
||||
size="default"
|
||||
className={`tw-font-normal ${response === true ? 'tw-text-text-success' : 'tw-text-text-warning'}`}
|
||||
className={`tw-font-normal ${response === true ? 'tw-text-text-success' : '!tw-text-text-warning'}`}
|
||||
data-cy="validation-label"
|
||||
>
|
||||
{validationMessage}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import DynamicForm from '@/_components/DynamicForm';
|
||||
import DynamicFormV2 from '@/_components/DynamicFormV2';
|
||||
import RunjsSchema from './Runjs.schema.json';
|
||||
import TooljetDbSchema from '@/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/manifest.json';
|
||||
import RunpySchema from './Runpy.schema.json';
|
||||
|
|
@ -7,12 +8,37 @@ import WorkflowsSchema from './Workflows.schema.json';
|
|||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { allManifests } from '@tooljet/plugins/client';
|
||||
import DataSourceSchemaManager from '@/_helpers/dataSourceSchemaManager';
|
||||
|
||||
const getSchemaDetailsForRender = (schema) => {
|
||||
if (schema['tj:version']) {
|
||||
const dsm = new DataSourceSchemaManager(schema);
|
||||
const initialSourceValues = dsm.getDefaults();
|
||||
return {
|
||||
name: schema['tj:source'].name,
|
||||
kind: schema['tj:source'].kind,
|
||||
type: schema['tj:source'].type,
|
||||
options: initialSourceValues,
|
||||
};
|
||||
}
|
||||
|
||||
const _source = schema.source;
|
||||
const def = schema.defaults ?? {};
|
||||
|
||||
return { ..._source, defaults: def };
|
||||
};
|
||||
|
||||
const getSchemaMetadata = (schema, key) => {
|
||||
if (schema['tj:version']) return schema['tj:source'][key];
|
||||
// Need to depreciate old schema format
|
||||
if (key === 'type') return schema.type;
|
||||
return schema.source[key];
|
||||
};
|
||||
|
||||
//Commonly Used DS
|
||||
|
||||
export const CommonlyUsedDataSources = Object.keys(allManifests)
|
||||
.reduce((accumulator, currentValue) => {
|
||||
const sourceName = allManifests[currentValue]?.source?.name;
|
||||
const sourceName = getSchemaMetadata(allManifests[currentValue], 'name');
|
||||
if (
|
||||
sourceName === 'REST API' ||
|
||||
sourceName === 'MongoDB' ||
|
||||
|
|
@ -20,9 +46,7 @@ export const CommonlyUsedDataSources = Object.keys(allManifests)
|
|||
sourceName === 'Google Sheets' ||
|
||||
sourceName === 'PostgreSQL'
|
||||
) {
|
||||
const _source = allManifests[currentValue].source;
|
||||
const def = allManifests[currentValue]?.defaults ?? {};
|
||||
accumulator.push({ ..._source, defaults: def });
|
||||
accumulator.push(getSchemaDetailsForRender(allManifests[currentValue]));
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
|
|
@ -33,31 +57,23 @@ export const CommonlyUsedDataSources = Object.keys(allManifests)
|
|||
});
|
||||
|
||||
export const DataBaseSources = Object.keys(allManifests).reduce((accumulator, currentValue) => {
|
||||
if (allManifests[currentValue].type === 'database') {
|
||||
const _source = allManifests[currentValue].source;
|
||||
const def = allManifests[currentValue]?.defaults ?? {};
|
||||
|
||||
accumulator.push({ ..._source, defaults: def });
|
||||
if (getSchemaMetadata(allManifests[currentValue], 'type') === 'database') {
|
||||
accumulator.push(getSchemaDetailsForRender(allManifests[currentValue]));
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}, []);
|
||||
export const ApiSources = Object.keys(allManifests).reduce((accumulator, currentValue) => {
|
||||
if (allManifests[currentValue].type === 'api') {
|
||||
const _source = allManifests[currentValue].source;
|
||||
const def = allManifests[currentValue]?.defaults ?? {};
|
||||
|
||||
accumulator.push({ ..._source, defaults: def });
|
||||
export const ApiSources = Object.keys(allManifests).reduce((accumulator, currentValue) => {
|
||||
if (getSchemaMetadata(allManifests[currentValue], 'type') === 'api') {
|
||||
accumulator.push(getSchemaDetailsForRender(allManifests[currentValue]));
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}, []);
|
||||
export const CloudStorageSources = Object.keys(allManifests).reduce((accumulator, currentValue) => {
|
||||
if (allManifests[currentValue].type === 'cloud-storage') {
|
||||
const _source = allManifests[currentValue].source;
|
||||
const def = allManifests[currentValue]?.defaults ?? {};
|
||||
|
||||
accumulator.push({ ..._source, defaults: def });
|
||||
if (getSchemaMetadata(allManifests[currentValue], 'type') === 'cloud-storage') {
|
||||
accumulator.push(getSchemaDetailsForRender(allManifests[currentValue]));
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
|
|
@ -73,8 +89,24 @@ export const DataSourceTypes = [
|
|||
];
|
||||
|
||||
export const SourceComponents = Object.keys(allManifests).reduce((accumulator, currentValue) => {
|
||||
accumulator[currentValue] = (props) => <DynamicForm schema={allManifests[currentValue]} isGDS={true} {...props} />;
|
||||
accumulator[currentValue] = (props) => {
|
||||
const schema = allManifests[currentValue];
|
||||
|
||||
if (schema['tj:version']) {
|
||||
return <DynamicFormV2 schema={schema} isGDS={true} {...props} />;
|
||||
}
|
||||
|
||||
return <DynamicForm schema={schema} isGDS={true} {...props} />;
|
||||
};
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
export const SourceComponent = (props) => <DynamicForm schema={props.dataSourceSchema} isGDS={true} {...props} />;
|
||||
export const SourceComponent = (props) => {
|
||||
const schema = props.dataSourceSchema;
|
||||
|
||||
if (schema['tj:version']) {
|
||||
return <DynamicFormV2 schema={schema} isGDS={true} {...props} />;
|
||||
}
|
||||
|
||||
return <DynamicForm schema={schema} isGDS={true} {...props} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { LicenseTooltip } from '@/LicenseTooltip';
|
|||
import { DATA_SOURCE_TYPE } from '@/_helpers/constants';
|
||||
import './dataSourceManager.theme.scss';
|
||||
import { canUpdateDataSource } from '@/_helpers';
|
||||
import DataSourceSchemaManager from '@/_helpers/dataSourceSchemaManager';
|
||||
import MultiEnvTabs from './MultiEnvTabs';
|
||||
|
||||
class DataSourceManagerComponent extends React.Component {
|
||||
|
|
@ -81,6 +82,8 @@ class DataSourceManagerComponent extends React.Component {
|
|||
unsavedChangesModal: false,
|
||||
datasourceName,
|
||||
creatingApp: false,
|
||||
validationError: [],
|
||||
validationMessages: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -208,8 +211,31 @@ class DataSourceManagerComponent extends React.Component {
|
|||
};
|
||||
|
||||
createDataSource = () => {
|
||||
const { appId, options, selectedDataSource, selectedDataSourcePluginId, dataSourceMeta, dataSourceSchema } =
|
||||
this.state;
|
||||
const {
|
||||
appId,
|
||||
options,
|
||||
selectedDataSource,
|
||||
selectedDataSourcePluginId,
|
||||
dataSourceMeta,
|
||||
dataSourceSchema,
|
||||
validationMessages,
|
||||
} = this.state;
|
||||
|
||||
if (!isEmpty(validationMessages)) {
|
||||
const validationMessageArray = Object.values(validationMessages);
|
||||
this.setState({ validationError: validationMessageArray });
|
||||
toast.error(
|
||||
this.props.t(
|
||||
'editor.queryManager.dataSourceManager.toast.error.validationFailed',
|
||||
'Validation failed. Please check your inputs.'
|
||||
),
|
||||
{ position: 'top-center' }
|
||||
);
|
||||
if (validationMessageArray.length > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const OAuthDs = ['slack', 'zendesk', 'googlesheets', 'salesforce'];
|
||||
const name = selectedDataSource.name;
|
||||
const kind = selectedDataSource?.kind;
|
||||
|
|
@ -231,6 +257,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
const value = localStorage.getItem('OAuthCode');
|
||||
parsedOptions.push({ key: 'code', value, encrypted: false });
|
||||
}
|
||||
|
||||
if (name.trim() !== '') {
|
||||
let service = scope === 'global' ? globalDatasourceService : datasourceService;
|
||||
if (selectedDataSource.id) {
|
||||
|
|
@ -335,6 +362,25 @@ class DataSourceManagerComponent extends React.Component {
|
|||
this.setState({ suggestingDatasources: true, activeDatasourceList: '#' });
|
||||
};
|
||||
|
||||
setValidationMessages = (errors, schema) => {
|
||||
const errorMap = errors.reduce((acc, error) => {
|
||||
// Get property name from either required error or dataPath
|
||||
const property =
|
||||
error.keyword === 'required'
|
||||
? error.params.missingProperty
|
||||
: error.dataPath?.replace(/^[./]/, '') || error.instancePath?.replace(/^[./]/, '');
|
||||
|
||||
if (property) {
|
||||
const propertySchema = schema.properties?.[property];
|
||||
const propertyTitle = propertySchema?.title;
|
||||
acc[property] =
|
||||
error.keyword === 'required' ? `${propertyTitle} is required` : `${propertyTitle} ${error.message}`;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
this.setState({ validationMessages: errorMap });
|
||||
};
|
||||
|
||||
renderSourceComponent = (kind, isPlugin = false) => {
|
||||
const { options, isSaving } = this.state;
|
||||
|
||||
|
|
@ -352,6 +398,9 @@ class DataSourceManagerComponent extends React.Component {
|
|||
selectedDataSource={this.state.selectedDataSource}
|
||||
isEditMode={!isEmpty(this.state.selectedDataSource)}
|
||||
currentAppEnvironmentId={this.props.currentEnvironment?.id}
|
||||
validationMessages={this.state.validationMessages}
|
||||
setValidationMessages={this.setValidationMessages}
|
||||
clearValidationMessages={() => this.setState({ validationMessages: {} })}
|
||||
setDefaultOptions={this.setDefaultOptions}
|
||||
/>
|
||||
);
|
||||
|
|
@ -851,6 +900,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
dataSourceConfirmModalProps,
|
||||
addingDataSource,
|
||||
datasourceName,
|
||||
validationError,
|
||||
} = this.state;
|
||||
const isPlugin = dataSourceSchema ? true : false;
|
||||
const createSelectedDataSource = (dataSource) => {
|
||||
|
|
@ -1056,6 +1106,18 @@ class DataSourceManagerComponent extends React.Component {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{validationError && validationError.length > 0 && (
|
||||
<div className="row w-100">
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{validationError.map((error, index) => (
|
||||
<div key={index} className="text-muted" data-cy="connection-alert-text">
|
||||
{error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="col">
|
||||
<SolidIcon name="logs" fill="#3E63DD" width="20" style={{ marginRight: '8px' }} />
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -474,7 +474,7 @@ export const GlobalDataSources = ({ darkMode = false, updateSelectedDatasource }
|
|||
<div className="row gx-0">
|
||||
<Sidebar renderSidebarList={renderSidebarList} updateSelectedDatasource={updateSelectedDatasource} />
|
||||
<div ref={containerRef} className={cx('col animation-fade datasource-modal-container', {})}>
|
||||
{containerRef && containerRef?.current && (
|
||||
{containerRef && containerRef?.current && selectedDataSource && (
|
||||
<DataSourceManager
|
||||
showBackButton={selectedDataSource ? false : true}
|
||||
showDataSourceManagerModal={showDataSourceManagerModal}
|
||||
|
|
|
|||
|
|
@ -1,72 +1,15 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="445.176" y="222.066" width="15.208" height="32.606"/>
|
||||
<path d="M465.275,103.536V55.237c0-10.89-8.859-19.75-19.749-19.75H292.721c-10.89,0-19.75,8.86-19.75,19.75v48.298h-38.498
|
||||
V55.237c0-10.89-8.86-19.75-19.75-19.75H61.918c-10.891,0-19.75,8.86-19.75,19.75v48.298H0v372.977h512V103.536H465.275z
|
||||
M288.178,55.237c0-2.505,2.038-4.542,4.542-4.542h152.805c2.504,0,4.541,2.038,4.541,4.542v48.298H422.47V64.268h-15.208v39.268
|
||||
H288.178V55.237z M57.374,55.237c0-2.505,2.038-4.542,4.542-4.542h152.804c2.504,0,4.542,2.038,4.542,4.542v48.298h-29.126
|
||||
V64.268h-15.208v39.268H57.374V55.237z M496.792,461.305h-36.404V269.298H445.18v192.007H15.208V118.743h26.959h192.305h38.498
|
||||
h192.305h31.517V461.305z"/>
|
||||
<path d="M385.777,275.757c-1.028-0.078-2.067-0.118-3.092-0.118c-10.329,0-20.117,3.942-27.562,11.096
|
||||
c-0.054,0.051-0.105,0.101-0.159,0.15v-81.028h-81.027c0.05-0.054,0.099-0.105,0.149-0.158
|
||||
c7.838-8.156,11.84-19.33,10.978-30.656c-1.449-19.026-16.24-34.353-35.171-36.443c-1.494-0.164-3.008-0.248-4.5-0.248
|
||||
c-21.939,0-39.787,17.848-39.787,39.788c0,10.414,3.994,20.254,11.25,27.719h-81.031v99.334h7.604
|
||||
c6.248,0,12.237-2.492,16.431-6.835c5.122-5.303,12.213-8.006,19.621-7.435c11.548,0.878,21.213,10.196,22.481,21.673
|
||||
c0.787,7.121-1.381,13.948-6.105,19.223c-4.661,5.204-11.337,8.189-18.317,8.189c-6.631,0-12.846-2.602-17.502-7.328
|
||||
c-3.186-3.232-7.177-5.463-11.542-6.449c-0.757-0.172-1.534-0.259-2.307-0.259c-5.715,0-10.364,4.639-10.364,10.343v88.681
|
||||
h88.991c5.703,0,10.343-4.639,10.343-10.342c0-6.248-2.492-12.238-6.836-16.432c-5.285-5.103-7.994-12.256-7.435-19.622
|
||||
c0.879-11.548,10.197-21.212,21.675-22.48c0.943-0.105,1.895-0.157,2.831-0.157c13.553,0,24.58,11.027,24.58,24.58
|
||||
c0,6.606-2.585,12.805-7.281,17.456c-4.555,4.514-7.065,10.396-7.065,16.565c0,5.753,4.68,10.433,10.434,10.433h88.9v-81.031
|
||||
c7.465,7.255,17.304,11.25,27.719,11.25c11.299,0,22.105-4.83,29.646-13.251c7.537-8.415,11.142-19.728,9.893-31.037
|
||||
C420.13,291.995,404.803,277.205,385.777,275.757z M401.001,331.817c-4.661,5.205-11.338,8.19-18.317,8.19
|
||||
c-6.457,0-12.557-2.486-17.174-7c-4.843-4.737-11.287-7.344-18.148-7.344h-7.604v84.126h-67.227
|
||||
c0.262-0.342,0.551-0.671,0.868-0.987c7.6-7.528,11.785-17.564,11.785-28.259c0-21.94-17.848-39.788-39.788-39.788
|
||||
c-1.491,0-3.005,0.084-4.5,0.248c-18.931,2.093-33.722,17.419-35.17,36.442c-0.905,11.898,3.482,23.458,12.036,31.718
|
||||
c0.202,0.195,0.391,0.404,0.569,0.625h-67.295v-64.691c7.3,6.542,16.618,10.117,26.507,10.117
|
||||
c11.298,0,22.104-4.83,29.646-13.251c7.537-8.415,11.142-19.728,9.892-31.038c-2.092-18.93-17.419-33.72-36.444-35.168
|
||||
c-1.027-0.078-2.068-0.118-3.092-0.118c-9.906,0-19.212,3.571-26.508,10.116v-64.689l84.126-0.005v-7.604
|
||||
c0-6.856-2.608-13.299-7.344-18.144c-4.515-4.618-7.001-10.716-7.001-17.174c0-13.553,11.026-24.58,24.579-24.58
|
||||
c0.936,0,1.888,0.053,2.831,0.157c11.478,1.267,20.796,10.932,21.675,22.481c0.541,7.117-1.867,13.851-6.78,18.964
|
||||
c-4.831,5.026-7.491,11.525-7.491,18.3v7.604h84.126v84.126h7.604c6.775,0,13.273-2.66,18.301-7.491
|
||||
c5.061-4.863,11.901-7.314,18.963-6.78c11.548,0.878,21.213,10.196,22.481,21.675
|
||||
C407.893,319.717,405.725,326.542,401.001,331.817z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_519_49)">
|
||||
<path d="M16.9116 2.39346H7.08579C6.70857 2.39346 6.4032 2.70255 6.4032 3.08759V3.09021V5.3743H17.5967V3.09021C17.5967 2.70516 17.2939 2.39346 16.9167 2.39346C16.9141 2.39346 16.9141 2.39346 16.9116 2.39346Z" fill="#BC3618"/>
|
||||
<path d="M18.0689 3.78959H5.93102C5.387 3.78959 4.94305 4.24012 4.94305 4.79542V4.79804V5.59171H19.0594V4.79804C19.0594 4.24274 18.6181 3.78959 18.074 3.78959C18.0715 3.78959 18.0715 3.78959 18.0689 3.78959Z" fill="#D13816"/>
|
||||
<path d="M19.4932 5.21976H4.50686C3.96283 5.21976 3.51889 5.6703 3.51889 6.2256V6.22822V20.5929C3.51889 21.1508 3.96283 21.6039 4.51199 21.6066H19.488C20.0346 21.6066 20.4786 21.1534 20.4811 20.5929V6.22822C20.4811 5.67292 20.0398 5.22238 19.4932 5.21976Z" fill="#E15A18"/>
|
||||
<path d="M8.5229 6.6997C7.48361 6.6997 6.63935 7.56147 6.64191 8.62232C6.64448 9.68316 7.48618 10.5449 8.52547 10.5423C9.56476 10.5423 10.4065 9.68054 10.4065 8.6197C10.409 7.56147 9.56476 6.6997 8.5229 6.6997ZM8.5229 9.86652C7.848 9.86652 7.29885 9.30859 7.29885 8.61708C7.29885 7.92818 7.84544 7.36764 8.5229 7.36764C9.1978 7.36764 9.74695 7.92556 9.74695 8.61708C9.74695 9.30859 9.20036 9.86652 8.5229 9.86652C8.52547 9.86652 8.52547 9.86652 8.5229 9.86652Z" fill="#F7B192"/>
|
||||
<path d="M9.2311 10.3354H7.83511V14.233H9.2311V10.3354Z" fill="#F7B192"/>
|
||||
<path d="M17.4685 8.64852C17.4685 7.58767 16.6242 6.7259 15.5849 6.7259C14.5456 6.7259 13.7014 7.58767 13.7014 8.64852C13.7014 9.44481 14.1812 10.1573 14.9075 10.4428V11.7629L7.83516 14.1466H7.82233V16.5905C6.86002 16.9886 6.39812 18.1071 6.78817 19.0893C7.17823 20.0716 8.27397 20.5431 9.23628 20.1449C10.1986 19.7468 10.6605 18.6283 10.2704 17.6461C10.0831 17.172 9.71615 16.7974 9.25681 16.5983V15.0843L16.2906 12.7112L16.2752 12.6823H16.2906V10.4245C17.0014 10.1363 17.4685 9.43171 17.4685 8.64852ZM9.74694 18.3795C9.74694 19.0684 9.20035 19.6289 8.52289 19.6289C7.84542 19.6289 7.29883 19.071 7.29883 18.3795C7.29883 17.6906 7.84542 17.13 8.52289 17.13C9.20035 17.1327 9.74694 17.6906 9.74694 18.3795ZM15.5849 9.89272C14.91 9.89272 14.3609 9.33479 14.3609 8.64328C14.3609 7.95438 14.9075 7.39384 15.5849 7.39384C16.2598 7.39384 16.809 7.95176 16.809 8.64328C16.809 9.33479 16.2598 9.89272 15.5849 9.89272Z" fill="#FCDACB"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_519_49">
|
||||
<rect width="18" height="20" fill="white" transform="translate(3 2)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 2.4 KiB |
|
|
@ -104,7 +104,37 @@ export default class Bigquery implements QueryService {
|
|||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new QueryError('Query could not be completed', error.message, {});
|
||||
const errorMessage = error.message || "An unknown error occurred.";
|
||||
let errorDetails: any = {};
|
||||
|
||||
const errorSuggestions = {
|
||||
"notFound": "Check if the table or dataset exists in the specified location.",
|
||||
"accessDenied": "Verify that the service account has the necessary permissions.",
|
||||
"invalidQuery": "Check the SQL syntax and ensure that all referenced columns exist.",
|
||||
"rateLimitExceeded": "You are making too many requests. Try again after some time.",
|
||||
"backendError": "BigQuery encountered an internal error. Retry the request after some time.",
|
||||
"quotaExceeded": "You have exceeded your quota limits. Consider upgrading your plan or reducing query size.",
|
||||
"duplicate": "A resource with this name already exists. Try using a different name.",
|
||||
"badRequest": "Check the request parameters and ensure they are correctly formatted.",
|
||||
};
|
||||
|
||||
if (error && error instanceof Error) {
|
||||
const bigqueryError = error as any;
|
||||
errorDetails.error = bigqueryError;
|
||||
|
||||
const reason = bigqueryError.response?.status?.errorResult?.reason || "unknownError";
|
||||
errorDetails.reason = reason;
|
||||
errorDetails.message = errorMessage;
|
||||
errorDetails.jobId = bigqueryError.response?.jobReference?.jobId;
|
||||
errorDetails.location = bigqueryError.response?.jobReference?.location;
|
||||
errorDetails.query = bigqueryError.response?.configuration?.query?.query;
|
||||
|
||||
|
||||
const suggestion = errorSuggestions[reason];
|
||||
errorDetails.suggestion = suggestion;
|
||||
}
|
||||
|
||||
throw new QueryError('Query could not be completed', errorMessage, errorDetails);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,86 +1,179 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Postgresql datasource",
|
||||
"description": "A schema defining postgresql datasource",
|
||||
"type": "database",
|
||||
"source": {
|
||||
"type": "object",
|
||||
"tj:version": "1.0.0",
|
||||
"tj:source": {
|
||||
"name": "PostgreSQL",
|
||||
"kind": "postgresql",
|
||||
"options": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"type": "string"
|
||||
},
|
||||
"database": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"encrypted": true
|
||||
},
|
||||
"ca_cert": {
|
||||
"encrypted": true
|
||||
},
|
||||
"client_key": {
|
||||
"encrypted": true
|
||||
},
|
||||
"client_cert": {
|
||||
"encrypted": true
|
||||
},
|
||||
"root_cert": {
|
||||
"encrypted": true
|
||||
},
|
||||
"connection_options": {
|
||||
"type": "array"
|
||||
},
|
||||
"connection_string": {
|
||||
"type": "string",
|
||||
"encrypted": true
|
||||
}
|
||||
},
|
||||
"exposedVariables": {
|
||||
"isLoading": false,
|
||||
"data": {},
|
||||
"rawData": {}
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"connection_type": {
|
||||
"value": "manual"
|
||||
},
|
||||
"host": {
|
||||
"value": "localhost"
|
||||
},
|
||||
"port": {
|
||||
"value": 5432
|
||||
},
|
||||
"database": {
|
||||
"value": ""
|
||||
},
|
||||
"username": {
|
||||
"value": ""
|
||||
},
|
||||
"password": {
|
||||
"value": ""
|
||||
},
|
||||
"ssl_enabled": {
|
||||
"value": true
|
||||
},
|
||||
"ssl_certificate": {
|
||||
"value": "none"
|
||||
}
|
||||
"type": "database"
|
||||
},
|
||||
"properties": {
|
||||
"connection_type": {
|
||||
"label": "Connection type",
|
||||
"key": "connection_type",
|
||||
"type": "dropdown-component-flip",
|
||||
"type": "string",
|
||||
"title": "Connection type",
|
||||
"description": "Single select dropdown for connection_type",
|
||||
"enum": [
|
||||
"manual",
|
||||
"string"
|
||||
],
|
||||
"default": "manual"
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"title": "Host",
|
||||
"description": "Enter host",
|
||||
"default": "localhost"
|
||||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"title": "Port",
|
||||
"description": "Enter port",
|
||||
"default": 5432
|
||||
},
|
||||
"database": {
|
||||
"type": "string",
|
||||
"title": "Database name",
|
||||
"description": "Name of the database"
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"title": "Username",
|
||||
"description": "Enter username"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"title": "Password",
|
||||
"description": "Enter password"
|
||||
},
|
||||
"ssl_enabled": {
|
||||
"type": "boolean",
|
||||
"title": "SSL",
|
||||
"description": "Toggle for ssl_enabled",
|
||||
"default": true
|
||||
},
|
||||
"ssl_certificate": {
|
||||
"type": "string",
|
||||
"title": "SSL certificate",
|
||||
"description": "Single select dropdown for choosing certificates",
|
||||
"enum": [
|
||||
"ca_certificate",
|
||||
"self_signed",
|
||||
"none"
|
||||
],
|
||||
"default": "none"
|
||||
},
|
||||
"connection_string": {
|
||||
"type": "string",
|
||||
"title": "Connection string",
|
||||
"description": "postgres://username:password@hostname:port/database?sslmode=require"
|
||||
},
|
||||
"ca_cert": {
|
||||
"type": "string",
|
||||
"title": "CA Cert",
|
||||
"description": "Enter ca certificate"
|
||||
},
|
||||
"client_key": {
|
||||
"type": "string",
|
||||
"title": "Client Key",
|
||||
"description": "Enter client key"
|
||||
},
|
||||
"client_cert": {
|
||||
"type": "string",
|
||||
"title": "Client Cert",
|
||||
"description": "Enter client certificate"
|
||||
},
|
||||
"root_cert": {
|
||||
"type": "string",
|
||||
"title": "Root Cert",
|
||||
"description": "Enter root certificate"
|
||||
}
|
||||
},
|
||||
"tj:encrypted": [
|
||||
"password",
|
||||
"ca_cert",
|
||||
"client_key",
|
||||
"client_cert",
|
||||
"root_cert",
|
||||
"connection_string"
|
||||
],
|
||||
"required": [
|
||||
"connection_type"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"connection_type": {
|
||||
"const": "manual"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"host",
|
||||
"port",
|
||||
"username",
|
||||
"password",
|
||||
"ssl_certificate"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"ssl_certificate": {
|
||||
"const": "ca_certificate"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"ca_cert"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"ssl_certificate": {
|
||||
"const": "self_signed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"client_key",
|
||||
"client_cert",
|
||||
"root_cert"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"connection_type": {
|
||||
"const": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"connection_string"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"tj:ui:properties": {
|
||||
"connection_type": {
|
||||
"$ref": "#/properties/connection_type",
|
||||
"key": "connection_type",
|
||||
"label": "Connection type",
|
||||
"description": "Single select dropdown for connection_type",
|
||||
"widget": "dropdown-component-flip",
|
||||
"list": [
|
||||
{
|
||||
"name": "Manual connection",
|
||||
|
|
@ -94,10 +187,11 @@
|
|||
},
|
||||
"manual": {
|
||||
"ssl_certificate": {
|
||||
"label": "SSL certificate",
|
||||
"$ref": "#/properties/ssl_certificate",
|
||||
"key": "ssl_certificate",
|
||||
"type": "dropdown-component-flip",
|
||||
"label": "SSL certificate",
|
||||
"description": "Single select dropdown for choosing certificates",
|
||||
"widget": "dropdown-component-flip",
|
||||
"list": [
|
||||
{
|
||||
"value": "ca_certificate",
|
||||
|
|
@ -114,97 +208,104 @@
|
|||
],
|
||||
"commonFields": {
|
||||
"host": {
|
||||
"label": "Host",
|
||||
"$ref": "#/properties/host",
|
||||
"key": "host",
|
||||
"type": "text",
|
||||
"description": "Enter host"
|
||||
"label": "Host",
|
||||
"description": "Enter host",
|
||||
"widget": "text-v3",
|
||||
"required": true
|
||||
},
|
||||
"port": {
|
||||
"label": "Port",
|
||||
"$ref": "#/properties/port",
|
||||
"key": "port",
|
||||
"type": "text",
|
||||
"description": "Enter port"
|
||||
"label": "Port",
|
||||
"description": "Enter port",
|
||||
"widget": "text-v3",
|
||||
"required": true
|
||||
},
|
||||
"ssl_enabled": {
|
||||
"label": "SSL",
|
||||
"$ref": "#/properties/ssl_enabled",
|
||||
"key": "ssl_enabled",
|
||||
"type": "toggle",
|
||||
"description": "Toggle for ssl_enabled"
|
||||
"label": "SSL",
|
||||
"description": "Toggle for ssl_enabled",
|
||||
"widget": "toggle"
|
||||
},
|
||||
"database": {
|
||||
"label": "Database name",
|
||||
"$ref": "#/properties/database",
|
||||
"key": "database",
|
||||
"type": "text",
|
||||
"description": "Name of the database"
|
||||
"label": "Database name",
|
||||
"description": "Name of the database",
|
||||
"widget": "text-v3"
|
||||
},
|
||||
"username": {
|
||||
"label": "Username",
|
||||
"$ref": "#/properties/username",
|
||||
"key": "username",
|
||||
"type": "text",
|
||||
"description": "Enter username"
|
||||
"label": "Username",
|
||||
"description": "Enter username",
|
||||
"widget": "text-v3",
|
||||
"required": true
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"$ref": "#/properties/password",
|
||||
"key": "password",
|
||||
"type": "password",
|
||||
"description": "Enter password"
|
||||
"label": "Password",
|
||||
"description": "Enter password",
|
||||
"widget": "password-v3",
|
||||
"required": true
|
||||
},
|
||||
"connection_options": {
|
||||
"label": "Connection options",
|
||||
"$ref": "#/properties/connection_options",
|
||||
"key": "connection_options",
|
||||
"type": "react-component-headers",
|
||||
"width":"316px"
|
||||
"label": "Connection options",
|
||||
"widget": "react-component-headers",
|
||||
"width": "316px",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ca_certificate": {
|
||||
"ca_cert": {
|
||||
"label": "CA Cert",
|
||||
"$ref": "#/properties/ca_cert",
|
||||
"key": "ca_cert",
|
||||
"type": "textarea",
|
||||
"encrypted": true,
|
||||
"description": "Enter ca certificate"
|
||||
"label": "CA Cert",
|
||||
"description": "Enter ca certificate",
|
||||
"widget": "textarea"
|
||||
}
|
||||
},
|
||||
"self_signed": {
|
||||
"client_key": {
|
||||
"label": "Client Key",
|
||||
"$ref": "#/properties/client_key",
|
||||
"key": "client_key",
|
||||
"type": "textarea",
|
||||
"encrypted": true,
|
||||
"description": "Enter client key"
|
||||
"label": "Client Key",
|
||||
"description": "Enter client key",
|
||||
"widget": "textarea"
|
||||
},
|
||||
"client_cert": {
|
||||
"label": "Client Cert",
|
||||
"$ref": "#/properties/client_cert",
|
||||
"key": "client_cert",
|
||||
"type": "textarea",
|
||||
"encrypted": true,
|
||||
"description": "Enter client certificate"
|
||||
"label": "Client Cert",
|
||||
"description": "Enter client certificate",
|
||||
"widget": "textarea"
|
||||
},
|
||||
"root_cert": {
|
||||
"label": "Root Cert",
|
||||
"$ref": "#/properties/root_cert",
|
||||
"key": "root_cert",
|
||||
"type": "textarea",
|
||||
"encrypted": true,
|
||||
"description": "Enter root certificate"
|
||||
"label": "Root Cert",
|
||||
"description": "Enter root certificate",
|
||||
"widget": "textarea",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"string": {
|
||||
"connection_string": {
|
||||
"label": "Connection string",
|
||||
"$ref": "#/properties/connection_string",
|
||||
"key": "connection_string",
|
||||
"type": "text",
|
||||
"encrypted": true,
|
||||
"description": "postgres://username:password@hostname:port/database?sslmode=require"
|
||||
"label": "Connection string",
|
||||
"description": "postgres://username:password@hostname:port/database?sslmode=require",
|
||||
"widget": "text",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"host",
|
||||
"port",
|
||||
"username",
|
||||
"database",
|
||||
"password"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
3.7.0
|
||||
3.8.0
|
||||
|
|
|
|||
|
|
@ -193,5 +193,13 @@
|
|||
"author": "Tooljet",
|
||||
"timestamp": "Tue, 21 Jan 2025 16:55:28 GMT",
|
||||
"tags": ["AI"]
|
||||
},
|
||||
{
|
||||
"name": "azurerepos",
|
||||
"description": "api plugin from azurerepos",
|
||||
"version": "1.0.0",
|
||||
"id": "azurerepos",
|
||||
"author": "Tooljet",
|
||||
"timestamp": "Mon, 23 Dec 2024 11:57:30 GMT"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -123,6 +123,14 @@ export const buttonGroupConfig = {
|
|||
defaultValue: '#007bff',
|
||||
},
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'left',
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
selected: [1],
|
||||
|
|
@ -156,6 +164,7 @@ export const buttonGroupConfig = {
|
|||
disabledState: { value: '{{false}}' },
|
||||
selectedTextColor: { value: '#FFFFFF' },
|
||||
selectedBackgroundColor: { value: '#4368E3' },
|
||||
alignment: { value: 'left' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -143,6 +143,15 @@ export const imageConfig = {
|
|||
},
|
||||
accordian: 'Image',
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'center',
|
||||
},
|
||||
accordian: 'Image',
|
||||
},
|
||||
backgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Background',
|
||||
|
|
@ -179,11 +188,11 @@ export const imageConfig = {
|
|||
padding: {
|
||||
type: 'switch',
|
||||
displayName: 'Padding',
|
||||
validation: { schema: { type: 'string' }, defaultValue: 'default' },
|
||||
options: [
|
||||
{ displayName: 'Default', value: 'default' },
|
||||
{ displayName: 'Custom', value: 'custom' },
|
||||
],
|
||||
validation: { schema: { type: 'string' }, defaultValue: 'default' },
|
||||
accordian: 'Container',
|
||||
isFxNotRequired: true,
|
||||
},
|
||||
|
|
@ -244,7 +253,6 @@ export const imageConfig = {
|
|||
loadingState: { value: '{{false}}' },
|
||||
disabledState: { value: '{{false}}' },
|
||||
visibility: { value: '{{true}}' },
|
||||
visible: { value: '{{true}}' },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
|
|
@ -256,6 +264,7 @@ export const imageConfig = {
|
|||
boxShadow: { value: '0px 0px 0px 0px #00000090' },
|
||||
padding: { value: 'default' },
|
||||
customPadding: { value: '{{0}}' },
|
||||
alignment: { value: 'center' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -159,6 +159,14 @@ export const linkConfig = {
|
|||
],
|
||||
accordian: 'container',
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'left',
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {},
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -50,6 +50,14 @@ export const paginationConfig = {
|
|||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'left',
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
totalPages: null,
|
||||
|
|
@ -73,6 +81,7 @@ export const paginationConfig = {
|
|||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledState: { value: '{{false}}' },
|
||||
alignment: { value: 'left' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,6 +32,14 @@ export const svgImageConfig = {
|
|||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'left',
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
value: {},
|
||||
|
|
@ -50,6 +58,7 @@ export const svgImageConfig = {
|
|||
events: [],
|
||||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
alignment: { value: 'left' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,6 +38,14 @@ export const tagsConfig = {
|
|||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
alignment: {
|
||||
type: 'alignButtons',
|
||||
displayName: 'Alignment',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'left',
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {},
|
||||
definition: {
|
||||
|
|
@ -54,6 +62,7 @@ export const tagsConfig = {
|
|||
events: [],
|
||||
styles: {
|
||||
visibility: { value: '{{true}}' },
|
||||
alignment: { value: 'left' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,10 +15,20 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
|
|||
}
|
||||
|
||||
protected defineAbilityFor(can: AbilityBuilder<FeatureAbility>['can'], UserAllPermissions: UserAllPermissions): void {
|
||||
const { superAdmin, isAdmin } = UserAllPermissions;
|
||||
if (superAdmin || isAdmin) {
|
||||
// Admin or super admin and do all operations
|
||||
can([FEATURE_KEY.INSTALL, FEATURE_KEY.UPDATE, FEATURE_KEY.DELETE], Plugin);
|
||||
const { superAdmin, isAdmin, isBuilder } = UserAllPermissions;
|
||||
if (superAdmin || isAdmin || isBuilder) {
|
||||
// Admin, super admin and Builder can do all operations
|
||||
can(
|
||||
[
|
||||
FEATURE_KEY.INSTALL,
|
||||
FEATURE_KEY.UPDATE,
|
||||
FEATURE_KEY.DELETE,
|
||||
FEATURE_KEY.INSTALL_DEPENDENT_PLUGINS,
|
||||
FEATURE_KEY.UNINSTALL_PLUGINS,
|
||||
FEATURE_KEY.DEPENDENT_PLUGINS,
|
||||
],
|
||||
Plugin
|
||||
);
|
||||
}
|
||||
// These two operations are available to all
|
||||
can([FEATURE_KEY.GET_ONE, FEATURE_KEY.RELOAD, FEATURE_KEY.GET], Plugin);
|
||||
|
|
|
|||
|
|
@ -10,5 +10,8 @@ export const FEATURES: FeaturesConfig = {
|
|||
[FEATURE_KEY.INSTALL]: {},
|
||||
[FEATURE_KEY.RELOAD]: {},
|
||||
[FEATURE_KEY.UPDATE]: {},
|
||||
[FEATURE_KEY.DEPENDENT_PLUGINS]: {},
|
||||
[FEATURE_KEY.INSTALL_DEPENDENT_PLUGINS]: {},
|
||||
[FEATURE_KEY.UNINSTALL_PLUGINS]: {},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,4 +5,7 @@ export enum FEATURE_KEY {
|
|||
GET = 'get',
|
||||
GET_ONE = 'get_one',
|
||||
RELOAD = 'reload',
|
||||
DEPENDENT_PLUGINS = 'dependent_plugins',
|
||||
INSTALL_DEPENDENT_PLUGINS = 'install_dependent_plugins',
|
||||
UNINSTALL_PLUGINS = 'uninstall_plugins',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,8 +70,24 @@ export class PluginsController implements IPluginsController {
|
|||
return this.pluginsService.reload(id);
|
||||
}
|
||||
|
||||
@Post('/findDepedentPlugins')
|
||||
@Post('findDependentPlugins')
|
||||
@InitFeature(FEATURE_KEY.DEPENDENT_PLUGINS)
|
||||
async findDependentPluginsToBeInstalledFromDataSources(@Body() dataSources) {
|
||||
return this.pluginsService.checkIfPluginsToBeInstalled(dataSources);
|
||||
}
|
||||
|
||||
@Post('installDependentPlugins')
|
||||
@InitFeature(FEATURE_KEY.INSTALL_DEPENDENT_PLUGINS)
|
||||
async installDependentPlugins(
|
||||
@Body('dependentPlugins') dependentPlugins,
|
||||
@Body('shouldAutoImportPlugin') shouldAutoImportPlugin
|
||||
) {
|
||||
return this.pluginsService.autoInstallPluginsForTemplates(dependentPlugins, shouldAutoImportPlugin);
|
||||
}
|
||||
|
||||
@Post('uninstallPlugins')
|
||||
@InitFeature(FEATURE_KEY.UNINSTALL_PLUGINS)
|
||||
async uninstallPlugins(@Body('pluginsId') pluginsId) {
|
||||
return this.pluginsService.uninstallPlugins(pluginsId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export class PluginsService implements IPluginsService {
|
|||
return Array.from(marketplacePluginsUsed);
|
||||
}
|
||||
|
||||
private async arePluginsInstalled(pluginsId: Array<string>): Promise<{ pluginsToBeInstalled: Array<string> }> {
|
||||
private async findPluginsToBeInstalled(pluginsId: Array<string>): Promise<{ pluginsToBeInstalled: Array<string> }> {
|
||||
const pluginsToBeInstalled = [];
|
||||
if (!pluginsId.length) return { pluginsToBeInstalled };
|
||||
|
||||
|
|
@ -154,30 +154,62 @@ export class PluginsService implements IPluginsService {
|
|||
async checkIfPluginsToBeInstalled(
|
||||
dataSources
|
||||
): Promise<{ pluginsToBeInstalled: Array<string>; pluginsListIdToDetailsMap: any }> {
|
||||
const { pluginsListIdToDetailsMap } = this.listMarketplacePlugins();
|
||||
const marketplacePluginsUsed = this.filterMarketplacePluginsFromDatasources(dataSources, pluginsListIdToDetailsMap);
|
||||
const { pluginsToBeInstalled } = await this.arePluginsInstalled(marketplacePluginsUsed);
|
||||
return { pluginsToBeInstalled, pluginsListIdToDetailsMap };
|
||||
}
|
||||
|
||||
async autoInstallPluginsForTemplates(pluginsToBeInstalled: Array<string>, shouldAutoInstall: boolean) {
|
||||
const { pluginsListIdToDetailsMap } = this.listMarketplacePlugins();
|
||||
if (shouldAutoInstall && pluginsToBeInstalled.length) {
|
||||
const installedPluginsName = [];
|
||||
for (const pluginId of pluginsToBeInstalled) {
|
||||
const pluginDetails = pluginsListIdToDetailsMap[pluginId];
|
||||
const installedPlugin = await this.install(pluginDetails);
|
||||
installedPluginsName.push(installedPlugin.name);
|
||||
}
|
||||
return installedPluginsName;
|
||||
}
|
||||
|
||||
if (!shouldAutoInstall && pluginsToBeInstalled.length) {
|
||||
throw new NotFoundException(
|
||||
`Plugins ( ${pluginsToBeInstalled
|
||||
.map((pluginToBeInstalled) => pluginsListIdToDetailsMap[pluginToBeInstalled].name || pluginToBeInstalled)
|
||||
.join(', ')} ) is not installed yet!`
|
||||
try {
|
||||
const { pluginsListIdToDetailsMap } = this.listMarketplacePlugins();
|
||||
const marketplacePluginsUsed = this.filterMarketplacePluginsFromDatasources(
|
||||
dataSources,
|
||||
pluginsListIdToDetailsMap
|
||||
);
|
||||
const { pluginsToBeInstalled } = await this.findPluginsToBeInstalled(marketplacePluginsUsed);
|
||||
return { pluginsToBeInstalled, pluginsListIdToDetailsMap };
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(
|
||||
error,
|
||||
'An error occurred while checking whether plugins need to be installed.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async autoInstallPluginsForTemplates(pluginsToBeInstalled: Array<string>, shouldAutoInstall: boolean) {
|
||||
const installedPluginsList = [];
|
||||
const installedPluginsInfo = [];
|
||||
try {
|
||||
const { pluginsListIdToDetailsMap } = this.listMarketplacePlugins();
|
||||
if (shouldAutoInstall && pluginsToBeInstalled.length) {
|
||||
for (const pluginId of pluginsToBeInstalled) {
|
||||
const pluginDetails = pluginsListIdToDetailsMap[pluginId];
|
||||
const installedPluginInfo = await this.install(pluginDetails);
|
||||
installedPluginsList.push(installedPluginInfo.name);
|
||||
installedPluginsInfo.push(installedPluginInfo);
|
||||
}
|
||||
return { installedPluginsList, installedPluginsInfo };
|
||||
}
|
||||
|
||||
if (!shouldAutoInstall && pluginsToBeInstalled.length) {
|
||||
throw new NotFoundException(
|
||||
`Plugins ( ${pluginsToBeInstalled
|
||||
.map((pluginToBeInstalled) => pluginsListIdToDetailsMap[pluginToBeInstalled].name || pluginToBeInstalled)
|
||||
.join(', ')} ) is not installed yet!`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (installedPluginsInfo.length) {
|
||||
const pluginsId = installedPluginsInfo.map((pluginInfo) => pluginInfo.id);
|
||||
await this.uninstallPlugins(pluginsId);
|
||||
}
|
||||
throw new InternalServerErrorException(error, 'Error while installing marketplace plugins');
|
||||
}
|
||||
}
|
||||
|
||||
async uninstallPlugins(pluginsId: Array<string>) {
|
||||
try {
|
||||
if (!pluginsId.length) return;
|
||||
for (const pluginId of pluginsId) {
|
||||
await this.remove(pluginId);
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(error, 'Error while uninstalling marketplace plugins');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ interface Features {
|
|||
[FEATURE_KEY.INSTALL]: FeatureConfig;
|
||||
[FEATURE_KEY.RELOAD]: FeatureConfig;
|
||||
[FEATURE_KEY.UPDATE]: FeatureConfig;
|
||||
[FEATURE_KEY.DEPENDENT_PLUGINS]: FeatureConfig;
|
||||
[FEATURE_KEY.INSTALL_DEPENDENT_PLUGINS]: FeatureConfig;
|
||||
[FEATURE_KEY.UNINSTALL_PLUGINS]: FeatureConfig;
|
||||
}
|
||||
|
||||
export interface FeaturesConfig {
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ export class TemplateAppsController {
|
|||
@User() user,
|
||||
@Body('identifier') identifier,
|
||||
@Body('appName') appName,
|
||||
@Body('dependentPluginsForTemplate') dependentPluginsForTemplate,
|
||||
@Body('dependentPlugins') dependentPlugins,
|
||||
@Body('shouldAutoImportPlugin') shouldAutoImportPlugin
|
||||
) {
|
||||
const newApp = await this.templatesService.perform(
|
||||
user,
|
||||
identifier,
|
||||
appName,
|
||||
dependentPluginsForTemplate,
|
||||
dependentPlugins,
|
||||
shouldAutoImportPlugin
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ export class TemplatesService {
|
|||
currentUser: User,
|
||||
identifier: string,
|
||||
appName: string,
|
||||
dependentPluginsForTemplate: Array<string>,
|
||||
dependentPlugins: Array<string>,
|
||||
shouldAutoImportPlugin: boolean
|
||||
) {
|
||||
const templateDefinition = this.findTemplateDefinition(identifier);
|
||||
if (dependentPluginsForTemplate.length)
|
||||
await this.pluginsService.autoInstallPluginsForTemplates(dependentPluginsForTemplate, shouldAutoImportPlugin);
|
||||
if (dependentPlugins.length)
|
||||
await this.pluginsService.autoInstallPluginsForTemplates(dependentPlugins, shouldAutoImportPlugin);
|
||||
return this.importTemplate(currentUser, templateDefinition, appName, identifier);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
IsIn,
|
||||
} from 'class-validator';
|
||||
import { sanitizeInput, formatTimestamp, validateDefaultValue, formatJSONB } from 'src/helpers/utils.helper';
|
||||
import { TooljetDatabaseDataTypes, TJDB } from '../types';
|
||||
|
||||
export function Match(property: string, validationOptions?: ValidationOptions) {
|
||||
return (object: any, propertyName: string) => {
|
||||
|
|
@ -189,11 +190,11 @@ export class PostgrestTableColumnDto {
|
|||
@Validate(SQLInjectionValidator, { message: 'Column name does not support special characters' })
|
||||
column_name: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(Object.values(TJDB), { message: 'Incorrect datatype.' })
|
||||
@IsNotEmpty()
|
||||
@Transform(({ value }) => sanitizeInput(value))
|
||||
@Validate(SQLInjectionValidator)
|
||||
data_type: string;
|
||||
data_type: TooljetDatabaseDataTypes;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value, obj }) => {
|
||||
|
|
@ -290,11 +291,11 @@ export class EditColumnTableDto {
|
|||
@Validate(SQLInjectionValidator, { message: 'Column name does not support special characters' })
|
||||
column_name: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(Object.values(TJDB), { message: 'Incorrect datatype.' })
|
||||
@IsNotEmpty()
|
||||
@Transform(({ value }) => sanitizeInput(value))
|
||||
@Validate(SQLInjectionValidator)
|
||||
data_type: string;
|
||||
data_type: TooljetDatabaseDataTypes;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value, obj }) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { QueryFailedError } from 'typeorm';
|
||||
import { InternalTable } from 'src/entities/internal_table.entity';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
export const TJDB = {
|
||||
character_varying: 'character varying' as const,
|
||||
|
|
@ -150,10 +149,11 @@ export class TooljetDatabaseError extends QueryFailedError {
|
|||
}
|
||||
|
||||
toString(): string {
|
||||
const capitalizeSentence = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
||||
const errorMessage =
|
||||
errorCodeMapping[this.code]?.[this.context.origin] ||
|
||||
errorCodeMapping[this.code]?.['default'] ||
|
||||
capitalize(this.message);
|
||||
capitalizeSentence(this.message);
|
||||
return this.replaceErrorPlaceholders(errorMessage);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue