Merge branch 'modularisation/v3' into fix/codehinter-search-replace

This commit is contained in:
devanshu052000 2025-03-10 12:35:59 +05:30
commit a3f25097f5
32 changed files with 360 additions and 103 deletions

View file

@ -5,19 +5,13 @@ const MIN_TABLE_ROW_HEIGHT_DEFAULT = 45;
const TableRowHeightInput = ({ value, onChange, cyLabel, staticText, styleDefinition }) => {
const [inputValue, setInputValue] = useState(value);
const minValue =
styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT;
useEffect(() => {
setInputValue(value < minValue ? minValue : value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, styleDefinition.cellSize?.value]);
useEffect(() => {
onChange(
styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const minValue =
styleDefinition.cellSize?.value === 'condensed' ? MIN_TABLE_ROW_HEIGHT_CONDENSED : MIN_TABLE_ROW_HEIGHT_DEFAULT;
const handleBlur = () => {
const newValue = Math.max(inputValue, minValue);

View file

@ -150,7 +150,7 @@ const TJDBCodeEditor = (props) => {
className="cm-codehinter position-relative"
style={{
width: '100%',
height: isOpen ? '350p' : 'auto',
height: isOpen ? '350px' : 'auto',
}}
>
<div className={`cm-codehinter ${darkMode && 'cm-codehinter-dark-themed'}`}>
@ -167,14 +167,14 @@ const TJDBCodeEditor = (props) => {
componentName={componentName}
key={componentName}
forceUpdate={forceUpdate}
optionalProps={{ styles: { height: 300 }, cls: '' }}
optionalProps={{ styles: { height: 300 }, cls: 'tjdb-hinter-portal' }}
darkMode={darkMode}
selectors={{ className: 'preview-block-portal tjdb-portal-codehinter' }}
dragResizePortal={true}
callgpt={null}
>
<ErrorBoundary>
<div className={`${errorState && 'tjdb-hinter-error'}`} data-cy={`${cyLabel}-input-field`}>
<div className={`${errorState && 'tjdb-hinter-error'} h-100`} data-cy={`${cyLabel}-input-field`}>
<CodeMirror
value={currentValue}
placeholder={placeholder}

View file

@ -653,4 +653,10 @@
.cm-searchMatch.cm-searchMatch-selected {
background-color: #F28F2D !important;
}
.tjdb-hinter-portal{
.cm-theme{
height: 100% ;
}
}

View file

@ -88,9 +88,19 @@ const LeftSidebarInspector = ({ darkMode, pinned, setPinned }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortedComponents, sortedQueries, sortedVariables, sortedConstants, sortedPageVariables, sortedGlobalVariables]);
const handleNodeExpansion = (path) => {
const handleNodeExpansion = (path, data, currentNode) => {
if (pathToBeInspected && path?.length > 0) {
return pathToBeInspected.includes(path[path.length - 1]);
const shouldExpand = pathToBeInspected.includes(path[path.length - 1]);
// Scroll to the component in the inspector
if (path?.length === 2 && path?.[0] === 'components' && shouldExpand) {
const target = document.getElementById(`inspector-node-${String(currentNode).toLowerCase()}`);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
return shouldExpand;
} else return false;
};

View file

@ -9,6 +9,7 @@ const useCallbackActions = () => {
const currentPageComponents = useStore((state) => state?.getCurrentPageComponents(), shallow);
const shouldFreeze = useStore((state) => state.getShouldFreeze());
const runQuery = useStore((state) => state.queryPanel.runQuery);
const getComponentIdToAutoScroll = useStore((state) => state.getComponentIdToAutoScroll);
const handleRemoveComponent = (component) => {
deleteComponents([component.id]);
@ -30,30 +31,22 @@ const useCallbackActions = () => {
return toast.success('Copied to the clipboard', { position: 'top-center' });
};
const autoScrollTo = (id) => {
setSelectedComponents([id]);
const target = document.getElementById(id);
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
};
const handleAutoScrollToComponent = (data) => {
const currentPageComponents = useStore.getState().getCurrentPageComponents();
const component = currentPageComponents?.[data.id];
let parentId = component?.component?.parent;
if (parentId) {
const regex = /-\d+$/;
if (regex.test(parentId)) {
parentId = parentId.replace(regex, ''); // To get parentId without tab index if parent type is Tab
}
const parentType = currentPageComponents?.[parentId]?.component?.component;
if (parentType && (parentType === 'Modal' || parentType === 'Tabs')) {
autoScrollTo(parentId); // To scroll to parent component if parent type is Modal or Tabs
return;
}
const { isAccessible, computedComponentId, isOnCanvas } = getComponentIdToAutoScroll(data.id);
if (!isAccessible) {
if (isOnCanvas) {
toast.success(
`This component can't be opened because it's on the main canvas. Close ${computedComponentId} and click "Go to component" to view it there`
);
} else
toast.success(
`This component can't be opened because it's inside ${computedComponentId}. Open ${computedComponentId} and click "Go to component"to view it.`
);
return;
}
autoScrollTo(data.id);
setSelectedComponents([computedComponentId]);
const target = document.getElementById(computedComponentId);
target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
};
const callbackActions = [

View file

@ -1,21 +1,31 @@
import React from 'react';
import OverflowTooltip from '@/_components/OverflowTooltip';
export const BaseUrl = ({ dataSourceURL, theme }) => {
export const BaseUrl = ({ dataSourceURL, theme, className = 'col-auto', style = {} }) => {
return (
<span
className="col-auto"
htmlFor=""
className={`${className} base-url-container`}
style={{
padding: '5px',
border: theme === 'default' ? '1px solid rgb(217 220 222)' : '1px solid white',
borderRightWidth: 0,
background: theme === 'default' ? 'rgb(246 247 251)' : '#20211e',
color: theme === 'default' ? '#9ca1a6' : '#9e9e9e',
height: '32px',
borderRadius: '6px 0 0 6px',
display: 'flex',
transition: 'height 0.2s ease',
...style,
}}
>
{dataSourceURL}
<OverflowTooltip
text={dataSourceURL}
width="559px"
whiteSpace="normal"
placement="auto"
style={{ height: '100%' }}
>
{dataSourceURL}
</OverflowTooltip>
</span>
);
};

View file

@ -28,7 +28,10 @@ class Restapi extends React.Component {
this.state = {
options,
codeHinterHeight: 32, // Default height
};
this.codeHinterRef = React.createRef();
this.resizeObserver = null;
}
componentDidUpdate(prevProps) {
@ -40,21 +43,95 @@ class Restapi extends React.Component {
},
});
}
// Setup resize observer if it's not already set up
if (this.codeHinterRef.current && !this.resizeObserver) {
this.setupResizeObserver();
}
}
componentDidMount() {
try {
if (isEmpty(this.state.options['headers'])) {
this.addNewKeyValuePair('headers');
}
if (isEmpty(this.state.options['cookies'])) {
this.addNewKeyValuePair('cookies');
}
if (isEmpty(this.state.options['method'])) {
changeOption(this, 'method', 'get');
}
setTimeout(() => {
if (isEmpty(this.state.options['url_params'])) {
this.addNewKeyValuePair('url_params');
}
}, 1000);
setTimeout(() => {
if (isEmpty(this.state.options['body'])) {
this.addNewKeyValuePair('body');
}
}, 1000);
setTimeout(() => {
this.initizalizeRetryNetworkErrorsToggle();
}, 1000);
this.setupResizeObserver();
} catch (error) {
console.log(error);
}
}
componentWillUnmount() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
}
setupResizeObserver() {
if (!this.codeHinterRef.current) return;
// Try to find the editor element, checking multiple possible selectors
const findEditorElement = () => {
const element =
this.codeHinterRef.current.querySelector('.cm-editor') ||
this.codeHinterRef.current.querySelector('.codehinter-input') ||
this.codeHinterRef.current.querySelector('.code-hinter-wrapper');
return element;
};
// Initial attempt to find editor
let editorElement = findEditorElement();
// If not found immediately, try again after a short delay
if (!editorElement) {
setTimeout(() => {
editorElement = findEditorElement();
if (editorElement) {
this.setupObserverForElement(editorElement);
}
}, 100);
return;
}
this.setupObserverForElement(editorElement);
}
setupObserverForElement(element) {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
this.resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const height = Math.max(32, Math.min(entry.contentRect.height, 220));
if (height !== this.state.codeHinterHeight) {
this.setState({ codeHinterHeight: height });
}
}
});
this.resizeObserver.observe(element);
}
initizalizeRetryNetworkErrorsToggle = () => {
const isRetryNetworkErrorToggleUnused = this.props.options.retry_network_errors === null;
if (isRetryNetworkErrorToggleUnused) {
@ -212,13 +289,30 @@ class Restapi extends React.Component {
useCustomStyles={true}
/>
</div>
<div className={`field w-100 rest-methods-url`}>
<div
className={`field rest-methods-url ${dataSourceURL && 'data-source-exists'}`}
style={{ width: 'calc(100% - 214px)' }}
>
<div className="font-weight-medium color-slate12">URL</div>
<div className="d-flex">
<div className="d-flex h-100 w-100">
{dataSourceURL && (
<BaseUrl theme={this.props.darkMode ? 'monokai' : 'default'} dataSourceURL={dataSourceURL} />
<BaseUrl
theme={this.props.darkMode ? 'monokai' : 'default'}
dataSourceURL={dataSourceURL}
style={{
overflowWrap: 'anywhere',
maxWidth: '40%',
width: 'fit-content',
height: `${this.state.codeHinterHeight}px`,
minHeight: '32px',
maxHeight: '220px',
}}
/>
)}
<div className={`flex-grow-1 rest-api-url-codehinter ${dataSourceURL ? 'url-input-group' : ''}`}>
<div
ref={this.codeHinterRef}
className={` flex-grow-1 rest-api-url-codehinter ${dataSourceURL ? 'url-input-group' : ''}`}
>
<CodeHinter
type="basic"
initialValue={options.url}

View file

@ -33,7 +33,7 @@ export const BulkUploadPrimaryKey = () => {
>
<input
type="text"
value={bulkUpdatePrimaryKey?.primary_key?.join() || ''}
value={bulkUpdatePrimaryKey?.primary_key?.join(', ') || ''}
style={{
width: '100%',
height: '100%',
@ -53,7 +53,7 @@ export const BulkUploadPrimaryKey = () => {
<div className="field flex-grow-1 minw-400-w-400">
<CodeHinter
type="basic"
initialValue={bulkUpdatePrimaryKey?.rows_update ?? {}}
initialValue={`{{${JSON.stringify(bulkUpdatePrimaryKey?.rows_update ?? [])}}}`}
className="codehinter-plugins"
placeholder="{{ [ { 'column1': 'value', ... } ] }}"
onChange={(newValue) => {

View file

@ -214,4 +214,9 @@
.input-value-padding {
box-sizing: border-box;
padding-right: 30px !important;
}
.react-datepicker__navigation{
overflow: visible !important;
height: inherit !important;
}

View file

@ -677,6 +677,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
}}
componentName="TooljetDatabase"
delayOnChange={false}
className="w-100"
/>
</div>
)}

View file

@ -56,6 +56,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) {
const [containers, setContainers] = useState([]);
const [showModal, setShowModal] = useState(false);
const setModalOpenOnCanvas = useStore((state) => state.setModalOpenOnCanvas);
const [activeId, setActiveId] = useState(null);
const cardMovementRef = useRef(null);
const shouldUpdateData = useRef(false);
@ -117,6 +118,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) {
}
/**** End - Logic to reduce the zIndex of modal control box ****/
}
setModalOpenOnCanvas(`${id}-modal`, showModal);
}, [showModal]);
useEffect(() => {

View file

@ -49,6 +49,7 @@ export const Modal = function Modal({
const size = properties.size ?? 'lg';
const [modalWidth, setModalWidth] = useState();
const mode = useStore((state) => state.currentMode, shallow);
const setModalOpenOnCanvas = useStore((state) => state.setModalOpenOnCanvas);
/**** Start - Logic to reset the zIndex of modal control box ****/
useEffect(() => {
@ -63,6 +64,7 @@ export const Modal = function Modal({
useGridStore.getState().actions.setOpenModalWidgetId(null);
}
}
setModalOpenOnCanvas(id, showModal);
}, [showModal, id, mode]);
/**** End - Logic to reset the zIndex of modal control box ****/

View file

@ -263,6 +263,9 @@ export const Datepicker = function Datepicker({
}
setIsDateInputFocussed(false);
}}
closeOnScroll={(e) => {
return e.target.className === 'table-responsive jet-data-table false false';
}}
/>
</div>
);

View file

@ -1114,10 +1114,9 @@ export const Table = React.memo(
<div
style={{
position: 'absolute',
top: 0,
top: `${items[0]?.start ?? 0}px`,
left: 0,
width: '100%',
transform: `translateY(${items[0]?.start ?? 0}px)`,
}}
>
{items.map((virtualRow) => {

View file

@ -44,6 +44,7 @@ const initialState = {
currentPageHandle: null,
showWidgetDeleteConfirmation: false,
focusedParentId: null,
modalsOpenOnCanvas: [],
};
export const createComponentsSlice = (set, get) => ({
@ -1867,4 +1868,17 @@ export const createComponentsSlice = (set, get) => ({
const currentPage = getCurrentPage(moduleId);
return currentPage?.autoComputeLayout;
},
setModalOpenOnCanvas: (modalId, isOpen) => {
const { modalsOpenOnCanvas } = get();
let newModalOpenOnCanvas = [];
if (isOpen) {
newModalOpenOnCanvas = [...modalsOpenOnCanvas, modalId];
} else {
newModalOpenOnCanvas = modalsOpenOnCanvas.filter((id) => id !== modalId);
}
set((state) => {
state.modalsOpenOnCanvas = newModalOpenOnCanvas;
});
},
});

View file

@ -37,4 +37,85 @@ export const createLeftSideBarSlice = (set, get) => ({
toggleLeftSidebar(true);
}
},
getComponentIdToAutoScroll: (componentId) => {
const { getCurrentPageComponents, getAllExposedValues, modalsOpenOnCanvas } = get();
const currentPageComponents = getCurrentPageComponents();
let targetComponentId = componentId;
let current = componentId;
const visited = new Set();
let isInsideOpenModal = false;
// Bubble up to the outermost parent to find the target component
// eslint-disable-next-line no-constant-condition
while (true) {
if (visited.has(current)) break;
visited.add(current);
const parentId = currentPageComponents?.[current]?.component?.parent;
if (!parentId) break;
let isComponentVisibleInParent = true;
let nextPossibleCandidate = parentId;
// If the component exists inside a tab component
const regForTabs = /-(?!\d{12}$)\d+$/; // Parent id for tabs follow the format 'id-index' and index is not UUIDv4 id segment
if (regForTabs.test(parentId)) {
const reg = /-(\d+)$/;
const tabIndex = Number(parentId.match(reg)[1]); // Tab index inside which the component exists
const tabId = parentId.replace(regForTabs, ''); // Extract tab id from parent id
const { currentTab } = getAllExposedValues().components?.[tabId] || {};
const activeTabIndex = Number(currentTab);
nextPossibleCandidate = tabId;
if (tabIndex !== activeTabIndex) {
isComponentVisibleInParent = false;
}
}
// If the component exists inside a modal component
if (currentPageComponents?.[parentId]?.component?.component === 'Modal') {
nextPossibleCandidate = parentId;
if (!modalsOpenOnCanvas.includes(parentId)) {
isComponentVisibleInParent = false;
}
}
// If the component exists inside the kanban component's modal
if (parentId.endsWith('-modal')) {
nextPossibleCandidate = parentId.replace(/-modal$/, ''); // Extract kanban id from parent id
if (!modalsOpenOnCanvas.includes(parentId)) {
isComponentVisibleInParent = false;
}
}
// If the open modal contains the component
if (modalsOpenOnCanvas[modalsOpenOnCanvas.length - 1] === parentId) {
isInsideOpenModal = true;
}
if (!isComponentVisibleInParent) {
targetComponentId = nextPossibleCandidate;
}
current = nextPossibleCandidate;
}
if (modalsOpenOnCanvas.length > 0 && !isInsideOpenModal) {
const targetId = visited.size === 1 ? modalsOpenOnCanvas[modalsOpenOnCanvas.length - 1] : current;
const componentName = currentPageComponents?.[targetId]?.component?.name;
return {
isAccessible: false,
computedComponentId: componentName,
isOnCanvas: visited.size === 1,
};
}
return {
isAccessible: true,
computedComponentId: targetComponentId,
};
},
});

View file

@ -42,7 +42,7 @@ export const CustomComponent = (props) => {
setCustomProps({ ...customPropRef.current, ...e.data.updatedObj });
} else if (e.data.message === 'RUN_QUERY') {
const options = {
parameters: e.data.parameters,
parameters: JSON.parse(e.data.parameters),
queryName: e.data.queryName,
};
onEvent('onTrigger', [], options);

View file

@ -436,6 +436,7 @@ export const DropdownV2 = ({
onChange={(selectedOption, actionProps) => {
if (actionProps.action === 'clear') {
setInputValue(null);
fireEvent('onSelect');
}
if (actionProps.action === 'select-option') {
setInputValue(selectedOption.value);

View file

@ -5,6 +5,7 @@ import JSONTreeViewer from '@/_ui/JSONTreeViewer';
import cx from 'classnames';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import useStore from '@/AppBuilder/_stores/store';
import { toast } from 'react-hot-toast';
function Logs({ logProps, idx }) {
const [open, setOpen] = React.useState(false);
@ -52,10 +53,19 @@ function Logs({ logProps, idx }) {
}
};
const copyToClipboard = (data) => {
const stringified = JSON.stringify(data, null, 2).replace(/\\/g, '');
navigator.clipboard.writeText(stringified);
return toast.success('Value copied to clipboard', { position: 'top-center' });
};
const callbackActions = [
{
for: 'all',
actions: [{ name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true }],
actions: [
{ name: 'Copy value', dispatchAction: copyToClipboard, icon: false },
{ name: 'Select Widget', dispatchAction: handleSelectComponentOnEditor, icon: false, onSelect: true },
],
enableForAllChildren: true,
enableFor1stLevelChildren: true,
},

View file

@ -163,7 +163,7 @@ export const DateTimePicker = ({
<div className={`fw-400 tjdbCellMenuShortcutsText`}>Save Changes</div>
</div>
<div className="d-flex align-items-center gap-1">
<div className={`fw-500 tjdbCellMenuShortcutsInfo`} id="escbutton">
<div className={`fw-500 tjdbCellMenuShortcutsInfo esc-btn-datepicker`} id="escbutton">
Esc
</div>
<div className={`fw-400 tjdbCellMenuShortcutsText`}>Discard Changes</div>

View file

@ -214,4 +214,24 @@
.input-value-padding {
box-sizing: border-box;
padding-right: 30px !important;
}
.datepicker-widget.theme-tjdb{
.react-datepicker__navigation{
overflow: visible !important;
height: inherit !important;
}
}
.esc-btn-datepicker{
height: 18px ;
align-items: center;
}
.tjdb-td-wrapper{
.react-datepicker-time__input{
input{
line-height: normal !important;
}
}
}

View file

@ -251,11 +251,7 @@ const Filter = ({
}
/>
<div className="tw-flex items-center tw-ml-[3px]">
{filterCount > 0 ? (
<span>{pluralize(validFilterCountRef.current, 'filter')}</span>
) : (
<div>&nbsp;&nbsp;Filter</div>
)}
{filterCount > 0 ? <span>{pluralize(filterCount, 'filter')}</span> : <div>&nbsp;&nbsp;Filter</div>}
</div>
{/* {areFiltersApplied && (
<span>ed by {pluralize(Object.values(filters).filter(checkIsFilterObjectEmpty).length, 'column')}</span>

View file

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { ToolTip } from '@/_components';
export default function OverflowTooltip({ children, className, whiteSpace = 'nowrap', ...rest }) {
export default function OverflowTooltip({ children, className, whiteSpace = 'nowrap', placement = 'bottom', ...rest }) {
const [isOverflowed, setIsOverflow] = useState(false);
const textElementRef = useRef();
@ -17,7 +17,7 @@ export default function OverflowTooltip({ children, className, whiteSpace = 'now
className={className}
delay={{ show: '0', hide: '0' }}
tooltipClassName="overflow-tooltip"
placement="bottom"
placement={placement}
message={children}
show={isOverflowed}
width={rest?.width}

View file

@ -3,13 +3,13 @@ import { authHeader, handleResponse, handleResponseWithoutValidation } from '@/_
function save(body) {
const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/custom-styles/`, requestOptions).then(handleResponse);
return fetch(`${config.apiUrl}/custom-styles`, requestOptions).then(handleResponse);
}
function get(validateResponse = true) {
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
const handleOutput = validateResponse ? handleResponse : handleResponseWithoutValidation;
return fetch(`${config.apiUrl}/custom-styles/`, requestOptions).then(handleOutput);
return fetch(`${config.apiUrl}/custom-styles`, requestOptions).then(handleOutput);
}
function getForAppViewerEditor(validateResponse = true) {

View file

@ -1250,6 +1250,11 @@ $border-radius: 4px;
color: var(--slate12) !important;
}
}
&.data-source-exists {
.cm-editor {
border-radius: 0 4px 4px 0 !important;
}
}
}
.rest-api-methods-select-element-container {

View file

@ -53,7 +53,7 @@ export const JSONNode = ({ data, ...restProps }) => {
React.useEffect(() => {
if (typeof shouldExpandNode === 'function') {
set(shouldExpandNode(path, data));
set(shouldExpandNode(path, data, currentNode));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathToBeInspected]);
@ -268,7 +268,15 @@ export const JSONNode = ({ data, ...restProps }) => {
};
return (
<div style={{ fontSize: '9px', marginTop: '0px', right: '10px' }} className="d-flex position-absolute">
<div
style={{
paddingLeft: !enableCopyToClipboard ? '13px' : '0px', // Temporary fix for hover issue for copy value button. Need to remove this once inspector gets revamped.
fontSize: '9px',
marginTop: '0px',
right: '10px',
}}
className="d-flex position-absolute"
>
{enableCopyToClipboard && (
<ToolTip message={'Copy to clipboard'}>
<span
@ -337,6 +345,7 @@ export const JSONNode = ({ data, ...restProps }) => {
'group-object-container': shouldDisplayIntendedBlock,
'mx-2': typeofCurrentNode !== 'Object' && typeofCurrentNode !== 'Array',
})}
id={`inspector-node-${String(currentNode).toLowerCase()}`}
data-cy={`inspector-node-${String(currentNode).toLowerCase()}`}
>
{$NODEIcon && <div className="json-tree-icon-container">{$NODEIcon}</div>}

View file

@ -9,6 +9,7 @@ import { utils } from '@/modules/common/helpers';
import { getSubpath } from '@/_helpers/routes';
import { TJLoader } from '@/_ui/TJLoader/TJLoader';
import useOnboardingStore from '@/modules/common/helpers/onboardingStoreHelper';
import useInvitationsStore from '@/modules/common/helpers/invitationStoreHelper';
const PostOnboardingComponent = () => <TJLoader />;
export const InvitationPage = (darkMode = false) => {
@ -24,7 +25,7 @@ export const InvitationPage = (darkMode = false) => {
const source = searchParams.get('source');
const redirectTo = searchParams.get('redirectTo');
const { initiateInvitedUserOnboarding } = invitationsStore();
const { initiateInvitedUserOnboarding } = useInvitationsStore();
const { resumeSignupOnboarding, isOnboardingStepsCompleted } = useOnboardingStore();
useEffect(() => {
// getUserDetails();

View file

@ -40,6 +40,7 @@ import { ImportExportResourcesModule } from '@modules/import-export-resources/mo
import { TooljetDbModule } from '@modules/tooljet-db/module';
import { WorkflowsModule } from '@modules/workflows/module';
import { AiModule } from '@modules/ai/module';
import { CustomStylesModule } from '@modules/custom-styles/module';
export class AppModule implements OnModuleInit {
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
@ -92,6 +93,7 @@ export class AppModule implements OnModuleInit {
await TooljetDbModule.register(configs),
await WorkflowsModule.register(configs),
await AiModule.register(configs),
await CustomStylesModule.register(configs),
];
return {

View file

@ -15,7 +15,13 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
return App;
}
protected defineAbilityFor(can: AbilityBuilder<FeatureAbility>['can'], UserAllPermissions: UserAllPermissions): void {
protected defineAbilityFor(
can: AbilityBuilder<FeatureAbility>['can'],
UserAllPermissions: UserAllPermissions,
extractedMetadata: { moduleName: string; features: string[] },
request?: any
): void {
const appId = request?.tj_resource_id;
const { superAdmin, isAdmin, userPermission } = UserAllPermissions;
const userAppPermissions = userPermission?.[MODULES.APP];
@ -51,7 +57,10 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
can(FEATURE_KEY.CREATE, App);
}
if (isAllAppsEditable) {
if (
isAllAppsEditable ||
(userAppPermissions?.editableAppsId?.length && appId && userAppPermissions.editableAppsId.includes(appId))
) {
can(
[
FEATURE_KEY.UPDATE,
@ -70,35 +79,14 @@ export class FeatureAbilityFactory extends AbilityFactory<FEATURE_KEY, Subjects>
can(FEATURE_KEY.DELETE, App);
}
return;
} else if (userAppPermissions?.editableAppsId?.length) {
can(
[
FEATURE_KEY.DELETE,
FEATURE_KEY.UPDATE_ICON,
FEATURE_KEY.GET_ONE,
FEATURE_KEY.GET_BY_SLUG,
FEATURE_KEY.RELEASE,
FEATURE_KEY.VALIDATE_PRIVATE_APP_ACCESS,
FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS,
FEATURE_KEY.UPDATE,
FEATURE_KEY.GET_ASSOCIATED_TABLES,
],
App,
{ id: { $in: userAppPermissions.editableAppsId } }
);
if (isAllAppsDeletable) {
// Gives delete permission only for editable apps
can(FEATURE_KEY.DELETE, App, { id: { $in: userAppPermissions.editableAppsId } });
}
}
if (isAllAppsViewable) {
// add view permissions for all apps
if (
isAllAppsViewable ||
(userAppPermissions?.viewableAppsId?.length && appId && userAppPermissions.viewableAppsId.includes(appId))
) {
// add view permissions for all apps or specific app
can([FEATURE_KEY.GET_ONE, FEATURE_KEY.GET_BY_SLUG, FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS], App);
} else if (userAppPermissions?.viewableAppsId?.length) {
can([FEATURE_KEY.GET_ONE, FEATURE_KEY.GET_BY_SLUG, FEATURE_KEY.VALIDATE_RELEASED_APP_ACCESS], App, {
id: { $in: userAppPermissions.viewableAppsId },
});
}
}
}

View file

@ -1,11 +1,21 @@
import { Module } from '@nestjs/common';
import { CustomStylesController } from '@modules/custom-styles/controller';
import { CustomStylesService } from '@modules/custom-styles/service';
import { DynamicModule } from '@nestjs/common';
import { getImportPath } from '@modules/app/constants';
import { OrganizationsModule } from '@modules/organizations/module';
import { FeatureAbilityFactory } from './ability';
import { OrganizationRepository } from '@modules/organizations/repository';
import { AppsRepository } from '@modules/apps/repository';
@Module({
imports: [],
providers: [CustomStylesService],
controllers: [CustomStylesController],
exports: [],
})
export class CustomStylesModule {}
export class CustomStylesModule {
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
const importPath = await getImportPath(configs?.IS_GET_CONTEXT);
const { CustomStylesController } = await import(`${importPath}/custom-styles/controller`);
const { CustomStylesService } = await import(`${importPath}/custom-styles/service`);
return {
module: CustomStylesModule,
imports: [await OrganizationsModule.register(configs)],
providers: [CustomStylesService, FeatureAbilityFactory, OrganizationRepository, AppsRepository],
controllers: [CustomStylesController],
exports: [],
};
}
}

View file

@ -7,7 +7,7 @@ export const FEATURES: FeaturesConfig = {
[MODULES.ORGANIZATION_THEMES]: {
[FEATURE_KEY.THEMES_CREATE]: { license: LICENSE_FIELD.CUSTOM_THEMES },
[FEATURE_KEY.THEMES_DELETE]: { license: LICENSE_FIELD.CUSTOM_THEMES },
[FEATURE_KEY.THEMES_GET_ALL]: { license: LICENSE_FIELD.CUSTOM_THEMES },
[FEATURE_KEY.THEMES_GET_ALL]: {},
[FEATURE_KEY.THEMES_UPDATE_DEFAULT]: { license: LICENSE_FIELD.CUSTOM_THEMES },
[FEATURE_KEY.THEMES_UPDATE_DEFINITION]: { license: LICENSE_FIELD.CUSTOM_THEMES },
[FEATURE_KEY.THEMES_UPDATE_NAME]: { license: LICENSE_FIELD.CUSTOM_THEMES },

View file

@ -65,6 +65,7 @@ export class UserRepository extends Repository<User> {
organizationId: true,
organization: {
name: true,
status: true,
},
},
},