);
diff --git a/frontend/src/AppBuilder/Widgets/Table/Table.jsx b/frontend/src/AppBuilder/Widgets/Table/Table.jsx
index 68b1609748..7d378e7d70 100644
--- a/frontend/src/AppBuilder/Widgets/Table/Table.jsx
+++ b/frontend/src/AppBuilder/Widgets/Table/Table.jsx
@@ -1114,10 +1114,9 @@ export const Table = React.memo(
{items.map((virtualRow) => {
diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js
index 14971be9d7..c4448a3bbf 100644
--- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js
@@ -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;
+ });
+ },
});
diff --git a/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js b/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js
index 98decac629..367ca4cf0c 100644
--- a/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/leftSideBarSlice.js
@@ -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,
+ };
+ },
});
diff --git a/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx b/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx
index 5bfe9818e5..4d4b7a2e48 100644
--- a/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx
+++ b/frontend/src/Editor/Components/CustomComponent/CustomComponent.jsx
@@ -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);
diff --git a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx
index 72e2b39664..a01ed895e0 100644
--- a/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx
+++ b/frontend/src/Editor/Components/DropdownV2/DropdownV2.jsx
@@ -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);
diff --git a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx
index 66548cbef8..aba2ca6af1 100644
--- a/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx
+++ b/frontend/src/Editor/LeftSidebar/SidebarDebugger/Logs.jsx
@@ -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,
},
diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx
index 40818a3bb9..7a4a0b0bce 100644
--- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx
+++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/DateTimePicker.jsx
@@ -163,7 +163,7 @@ export const DateTimePicker = ({
Save Changes
-
+
Esc
Discard Changes
diff --git a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
index 95d2ab56e0..55d0e7f3ed 100644
--- a/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
+++ b/frontend/src/Editor/QueryManager/QueryEditors/TooljetDatabase/DateTimePicker/styles.scss
@@ -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;
+ }
+ }
}
\ No newline at end of file
diff --git a/frontend/src/TooljetDatabase/Filter/index.jsx b/frontend/src/TooljetDatabase/Filter/index.jsx
index 84b0110fe4..6c8f7c811d 100644
--- a/frontend/src/TooljetDatabase/Filter/index.jsx
+++ b/frontend/src/TooljetDatabase/Filter/index.jsx
@@ -251,11 +251,7 @@ const Filter = ({
}
/>
- {filterCount > 0 ? (
-
{pluralize(validFilterCountRef.current, 'filter')}
- ) : (
-
Filter
- )}
+ {filterCount > 0 ?
{pluralize(filterCount, 'filter')} :
Filter
}
{/* {areFiltersApplied && (
ed by {pluralize(Object.values(filters).filter(checkIsFilterObjectEmpty).length, 'column')}
diff --git a/frontend/src/_components/OverflowTooltip.jsx b/frontend/src/_components/OverflowTooltip.jsx
index 297f17d236..1523ae449a 100644
--- a/frontend/src/_components/OverflowTooltip.jsx
+++ b/frontend/src/_components/OverflowTooltip.jsx
@@ -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}
diff --git a/frontend/src/_services/custom_styles.service.js b/frontend/src/_services/custom_styles.service.js
index d3c98d6382..73de321466 100644
--- a/frontend/src/_services/custom_styles.service.js
+++ b/frontend/src/_services/custom_styles.service.js
@@ -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) {
diff --git a/frontend/src/_styles/queryManager.scss b/frontend/src/_styles/queryManager.scss
index 8a9eaf972a..7b256c3812 100644
--- a/frontend/src/_styles/queryManager.scss
+++ b/frontend/src/_styles/queryManager.scss
@@ -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 {
diff --git a/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx b/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx
index 65a4ece91d..b13e88d767 100644
--- a/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx
+++ b/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx
@@ -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 (
-
+
{enableCopyToClipboard && (
{
'group-object-container': shouldDisplayIntendedBlock,
'mx-2': typeofCurrentNode !== 'Object' && typeofCurrentNode !== 'Array',
})}
+ id={`inspector-node-${String(currentNode).toLowerCase()}`}
data-cy={`inspector-node-${String(currentNode).toLowerCase()}`}
>
{$NODEIcon && {$NODEIcon}
}
diff --git a/frontend/src/modules/onboarding/pages/InvitationPage/InvitationPage.jsx b/frontend/src/modules/onboarding/pages/InvitationPage/InvitationPage.jsx
index f157f4744b..e2a4360943 100644
--- a/frontend/src/modules/onboarding/pages/InvitationPage/InvitationPage.jsx
+++ b/frontend/src/modules/onboarding/pages/InvitationPage/InvitationPage.jsx
@@ -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 = () => ;
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();
diff --git a/server/src/modules/app/module.ts b/server/src/modules/app/module.ts
index 1f3939cd32..c0ca97e4be 100644
--- a/server/src/modules/app/module.ts
+++ b/server/src/modules/app/module.ts
@@ -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 {
@@ -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 {
diff --git a/server/src/modules/apps/ability/index.ts b/server/src/modules/apps/ability/index.ts
index 4200a1c463..d53309202f 100644
--- a/server/src/modules/apps/ability/index.ts
+++ b/server/src/modules/apps/ability/index.ts
@@ -15,7 +15,13 @@ export class FeatureAbilityFactory extends AbilityFactory
return App;
}
- protected defineAbilityFor(can: AbilityBuilder['can'], UserAllPermissions: UserAllPermissions): void {
+ protected defineAbilityFor(
+ can: AbilityBuilder['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
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
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 },
- });
}
}
}
diff --git a/server/src/modules/custom-styles/module.ts b/server/src/modules/custom-styles/module.ts
index 9ff9dc290b..20608341f2 100644
--- a/server/src/modules/custom-styles/module.ts
+++ b/server/src/modules/custom-styles/module.ts
@@ -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 {
+ 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: [],
+ };
+ }
+}
diff --git a/server/src/modules/organization-themes/constants/feature.ts b/server/src/modules/organization-themes/constants/feature.ts
index 0fa5e3dfd3..25b8274fa4 100644
--- a/server/src/modules/organization-themes/constants/feature.ts
+++ b/server/src/modules/organization-themes/constants/feature.ts
@@ -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 },
diff --git a/server/src/modules/users/repository.ts b/server/src/modules/users/repository.ts
index 2771eb5d9b..0a4f77902a 100644
--- a/server/src/modules/users/repository.ts
+++ b/server/src/modules/users/repository.ts
@@ -65,6 +65,7 @@ export class UserRepository extends Repository {
organizationId: true,
organization: {
name: true,
+ status: true,
},
},
},