diff --git a/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx b/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx
index 229fb44175..abff6a9d7e 100644
--- a/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx
+++ b/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx
@@ -19,12 +19,12 @@ import useSidebarMargin from './Hooks/useSidebarMargin';
import useAppPageSidebarHeight from './Hooks/useAppPageSidebarHeight';
import { Container } from './Container';
import { SuspenseCountProvider } from './SuspenseTracker';
+import { MobileLayout } from './MobileLayout';
+import { DesktopLayout } from './DesktopLayout';
// Lazy load editor-only component to reduce viewer bundle size
const AppCanvasBanner = lazy(() => import('@/AppBuilder/Header/AppCanvasBanner'));
const EditorSelecto = React.lazy(() => import('./Selecto'));
const Grid = React.lazy(() => import('./Grid'));
-const MobileLayout = lazy(() => import('./MobileLayout').then((m) => ({ default: m.MobileLayout })));
-const DesktopLayout = lazy(() => import('./DesktopLayout').then((m) => ({ default: m.DesktopLayout })));
import useCanvasMinWidth from './Hooks/useCanvasMinWidth';
import useEnableMainCanvasScroll from './Hooks/useEnableMainCanvasScroll';
import useCanvasResizing from './Hooks/useCanvasResizing';
@@ -264,50 +264,48 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
onAllResolved={handleAllSuspenseResolved}
deferCheck={isModuleMode || appType === 'module'}
>
-
- {isMobileLayout ? (
-
- ) : (
-
- )}
-
+ {isMobileLayout ? (
+
+ ) : (
+
+ )}
)}
diff --git a/frontend/src/AppBuilder/AppCanvas/DesktopLayout.jsx b/frontend/src/AppBuilder/AppCanvas/DesktopLayout.jsx
index 427c0b8046..ed0cd38b04 100644
--- a/frontend/src/AppBuilder/AppCanvas/DesktopLayout.jsx
+++ b/frontend/src/AppBuilder/AppCanvas/DesktopLayout.jsx
@@ -1,12 +1,13 @@
-import React from 'react';
+import React, { Suspense, lazy } from 'react';
import cx from 'classnames';
import { PAGE_CANVAS_HEADER_HEIGHT } from './appCanvasConstants';
-import PageCanvasHeader from './PageCanvasHeader';
-import PageCanvasFooter from './PageCanvasFooter';
import PagesSidebarNavigation from './PageMenu/PagesSidebarNavigation';
import { CanvasContentTail } from './CanvasContentTail';
+const PageCanvasHeader = lazy(() => import('./PageCanvasHeader'));
+const PageCanvasFooter = lazy(() => import('./PageCanvasFooter'));
+
export const DesktopLayout = ({
pageKey,
isModuleMode,
@@ -35,7 +36,9 @@ export const DesktopLayout = ({
className={cx({ 'h-100': isModuleMode })}
style={{ position: 'relative', display: 'flex', flexDirection: 'column' }}
>
-
+
+
+
-
+
+
+
);
diff --git a/frontend/src/AppBuilder/AppCanvas/MobileLayout.jsx b/frontend/src/AppBuilder/AppCanvas/MobileLayout.jsx
index dd7d9d1896..62468e6bb4 100644
--- a/frontend/src/AppBuilder/AppCanvas/MobileLayout.jsx
+++ b/frontend/src/AppBuilder/AppCanvas/MobileLayout.jsx
@@ -1,12 +1,13 @@
-import React, { useRef } from 'react';
+import React, { Suspense, useRef, lazy } from 'react';
import cx from 'classnames';
import { PAGE_CANVAS_HEADER_HEIGHT } from './appCanvasConstants';
-import PageCanvasHeader from './PageCanvasHeader';
-import PageCanvasFooter from './PageCanvasFooter';
import MobileNavigationHeader from './PageMenu/MobileNavigationHeader';
import { CanvasContentTail } from './CanvasContentTail';
+const PageCanvasHeader = lazy(() => import('./PageCanvasHeader'));
+const PageCanvasFooter = lazy(() => import('./PageCanvasFooter'));
+
export const MobileLayout = ({
pageKey,
// mobileCanvasFrameRef,
@@ -41,7 +42,13 @@ export const MobileLayout = ({
className={cx('tw-absolute tw-inset-0 tw-overflow-hidden tw-pointer-events-none')}
/>
{/* Canvas header — sticky at top of scroll */}
-
+
+
+
{/* Mobile nav — sticky below header */}
{appType !== 'module' && (
{mainCanvasContainer}
-
+
+
+
);
};
diff --git a/frontend/src/AppBuilder/LeftSidebar/AppLibraries/AppLibrariesIcon.jsx b/frontend/src/AppBuilder/LeftSidebar/AppLibraries/AppLibrariesIcon.jsx
new file mode 100644
index 0000000000..5358db387a
--- /dev/null
+++ b/frontend/src/AppBuilder/LeftSidebar/AppLibraries/AppLibrariesIcon.jsx
@@ -0,0 +1,7 @@
+import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
+
+const AppLibrariesIcon = () => {
+ return null;
+};
+
+export default withEditionSpecificComponent(AppLibrariesIcon, 'AppLibraries');
diff --git a/frontend/src/AppBuilder/LeftSidebar/AppLibraries/index.jsx b/frontend/src/AppBuilder/LeftSidebar/AppLibraries/index.jsx
new file mode 100644
index 0000000000..72e40889a0
--- /dev/null
+++ b/frontend/src/AppBuilder/LeftSidebar/AppLibraries/index.jsx
@@ -0,0 +1,7 @@
+import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
+
+const AppLibraries = () => {
+ return null;
+};
+
+export default withEditionSpecificComponent(AppLibraries, 'AppLibraries');
diff --git a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/styles.scss b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/styles.scss
index f3b6d8b12c..1d8316bd98 100644
--- a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/styles.scss
+++ b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/styles.scss
@@ -267,4 +267,5 @@
width: 158px;
}
}
+
}
diff --git a/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx b/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx
index 7da3f8375b..0b70a5f5ef 100644
--- a/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx
+++ b/frontend/src/AppBuilder/LeftSidebar/LeftSidebar.jsx
@@ -22,6 +22,8 @@ import { useOthers, useSelf } from '@y-presence/react';
import { useAppDataActions, useAppInfo } from '@/_stores/appDataStore';
import AppHistoryIcon from './AppHistory/AppHistoryIcon';
import AppHistory from './AppHistory';
+import AppLibrariesIcon from './AppLibraries/AppLibrariesIcon';
+import AppLibraries from './AppLibraries';
import { APP_HEADER_HEIGHT, QUERY_PANE_HEIGHT } from '../AppCanvas/appCanvasConstants';
// TODO: remove passing refs to LeftSidebarItem and use state
@@ -115,6 +117,8 @@ export const BaseLeftSidebar = ({
return renderAIChat({ darkMode });
case 'apphistory':
return ;
+ case 'libraries':
+ return toggleLeftSidebar(false)} />;
case 'debugger':
return toggleLeftSidebar(false)} darkMode={darkMode} />;
case 'settings':
@@ -189,6 +193,14 @@ export const BaseLeftSidebar = ({
setSideBarBtnRefs={setSideBarBtnRefs}
/>
)}
+ {featureAccess?.appJsLibraries && (
+
+ )}
{
// if tooljetai is open don't close
- if (['tooljetai', 'inspect', 'debugger', 'settings'].includes(selectedSidebarItem)) return;
+ if (['tooljetai', 'inspect', 'debugger', 'settings', 'libraries'].includes(selectedSidebarItem)) return;
const isWithinSidebar = e.target.closest('.left-sidebar');
const isClickOnInspect = e.target.closest('.config-handle-inspect');
if (pinned || isWithinSidebar || isClickOnInspect) return;
diff --git a/frontend/src/AppBuilder/_helpers/libraryLoader.js b/frontend/src/AppBuilder/_helpers/libraryLoader.js
new file mode 100644
index 0000000000..36f647d67c
--- /dev/null
+++ b/frontend/src/AppBuilder/_helpers/libraryLoader.js
@@ -0,0 +1,122 @@
+import toast from 'react-hot-toast';
+import moment from 'moment';
+import _ from 'lodash';
+import axios from 'axios';
+
+/**
+ * Executes UMD/IIFE source code and captures the exported module.
+ * Creates a sandboxed environment with fake module/exports/define to capture
+ * the library export without polluting the global scope.
+ */
+function executeUMD(source) {
+ const module = { exports: {} };
+ const exports = module.exports;
+ let amdResult = null;
+
+ const define = Object.assign(
+ (...args) => {
+ // Handle: define(factory), define(deps, factory), define(id, deps, factory)
+ const factory = args.find((a) => typeof a === 'function');
+ if (factory) {
+ amdResult = factory();
+ return;
+ }
+ // Handle: define(value) — plain object/string export
+ const value = args[args.length - 1];
+ if (typeof value !== 'string') {
+ amdResult = value;
+ }
+ },
+ { amd: true }
+ );
+
+ const fn = new Function('module', 'exports', 'define', 'self', '"use strict";\n' + source);
+ fn(module, exports, define, {});
+
+ // Priority: AMD result > reassigned module.exports > properties added to exports
+ if (amdResult != null) return amdResult;
+ if (module.exports !== exports) return module.exports; // was reassigned (e.g. module.exports = factory())
+ if (Object.keys(exports).length > 0) return exports; // properties were added to original object
+ return null;
+}
+
+/**
+ * Fetches a JS library from a URL and loads it using the UMD shim.
+ * Returns the exported module object.
+ */
+export async function loadLibraryFromURL(url) {
+ const response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch library from ${url}: ${response.status} ${response.statusText}`);
+ }
+ const source = await response.text();
+ return executeUMD(source);
+}
+
+/**
+ * Initializes all JS libraries from the globalSettings.libraries.javascript array.
+ * Returns a registry object mapping library names to their exports.
+ */
+export async function initializeLibraries(jsLibraries = []) {
+ const registry = {};
+ const enabledLibraries = jsLibraries.filter((lib) => lib.enabled);
+
+ // Load all libraries in parallel for faster startup
+ const results = await Promise.allSettled(
+ enabledLibraries.map(async (lib) => {
+ const module = await loadLibraryFromURL(lib.url);
+ return { name: lib.name, module };
+ })
+ );
+
+ results.forEach((result, index) => {
+ const lib = enabledLibraries[index];
+ if (result.status === 'fulfilled') {
+ if (result.value.module != null) {
+ registry[result.value.name] = result.value.module;
+ } else {
+ console.warn(`Library "${lib.name}" loaded but exported nothing. Ensure it's a UMD/IIFE build.`);
+ }
+ } else {
+ console.error(`Failed to load library "${lib.name}" from ${lib.url}:`, result.reason);
+ toast.error(`Failed to load JS library "${lib.name}"`);
+ }
+ });
+
+ return registry;
+}
+
+/**
+ * Executes preloaded JavaScript and captures exported functions/variables.
+ * The code must return an object — each property becomes a top-level variable
+ * available in RunJS queries, transformations, and {{}} expressions.
+ *
+ * Libraries (both built-in and user-added) are available in scope.
+ * No access to components, queries, globals, etc.
+ *
+ * Example user code:
+ * function formatCurrency(amount) { return '$' + amount.toFixed(2); }
+ * const TAX_RATE = 0.08;
+ * return { formatCurrency, TAX_RATE };
+ */
+export async function executePreloadedJS(code, libraryRegistry = {}) {
+ if (!code?.trim()) return {};
+
+ try {
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
+ const fnParams = ['moment', '_', 'axios', ...Object.keys(libraryRegistry)];
+ const fnArgs = [moment, _, axios, ...Object.values(libraryRegistry)];
+ const fn = new AsyncFunction(...fnParams, code);
+ const result = await fn(...fnArgs);
+
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
+ return result;
+ }
+
+ return {};
+ } catch (error) {
+ console.error('Preloaded JS execution failed:', error);
+ toast.error('Preloaded JavaScript failed: ' + error.message);
+ return {};
+ }
+}
diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js
index b8cbf2b857..bcb6a769fa 100644
--- a/frontend/src/AppBuilder/_hooks/useAppData.js
+++ b/frontend/src/AppBuilder/_hooks/useAppData.js
@@ -27,6 +27,8 @@ import { useLocation, useParams } from 'react-router-dom';
import { useMounted } from '@/_hooks/use-mount';
import useThemeAccess from './useThemeAccess';
import toast from 'react-hot-toast';
+import { initializeLibraries, executePreloadedJS } from '@/AppBuilder/_helpers/libraryLoader';
+
/**
* this is to normalize the query transformation options to match the expected schema. Takes care of corrupted data.
* This will get redundanted once api response for appdata is made uniform across all the endpoints.
@@ -113,6 +115,8 @@ const useAppData = (
const setPageSwitchInProgress = useStore((state) => state.setPageSwitchInProgress);
const selectedVersion = useStore((state) => state.selectedVersion);
const setIsPublicAccess = useStore((state) => state.setIsPublicAccess);
+ const setJsLibraryRegistry = useStore((state) => state.setJsLibraryRegistry);
+ const setJsLibraryLoading = useStore((state) => state.setJsLibraryLoading);
const setModulesIsLoading = useStore((state) => state?.setModulesIsLoading ?? noop);
const setModulesList = useStore((state) => state?.setModulesList ?? noop);
@@ -596,10 +600,38 @@ const useAppData = (
useEffect(() => {
if (isComponentLayoutReady) {
mode === 'edit' && initSuggestions(moduleId);
- runOnLoadQueries(moduleId).then(() => {
+
+ const loadLibrariesAndRun = async () => {
+ // Load JS libraries and preloaded JS from globalSettings before running queries
+ const globalSettings = useStore.getState().globalSettings;
+ const jsLibraries = globalSettings?.libraries?.javascript || [];
+ const preloadedJS = globalSettings?.preloadedScript?.javascript || '';
+
+ const hasJSLibrariesAccess = useStore.getState().license?.featureAccess?.appJsLibraries;
+
+ if (hasJSLibrariesAccess && (jsLibraries.length > 0 || preloadedJS)) {
+ setJsLibraryLoading(true);
+ try {
+ const registry = jsLibraries.length > 0 ? await initializeLibraries(jsLibraries) : {};
+
+ // Execute preloaded JS — its returned exports merge into the registry
+ const preloadedExports = await executePreloadedJS(preloadedJS, registry);
+ const fullRegistry = { ...registry, ...preloadedExports };
+
+ setJsLibraryRegistry(fullRegistry);
+ } catch (error) {
+ console.error('Failed to initialize JS libraries:', error);
+ } finally {
+ setJsLibraryLoading(false);
+ }
+ }
+
+ await runOnLoadQueries(moduleId);
const currentPageEvents = events.filter((event) => event.target === 'page' && event.sourceId === currentPageId);
handleEvent('onPageLoad', currentPageEvents, {});
- });
+ };
+
+ loadLibrariesAndRun();
}
}, [isComponentLayoutReady, moduleId, mode]);
diff --git a/frontend/src/AppBuilder/_stores/slices/librarySlice.js b/frontend/src/AppBuilder/_stores/slices/librarySlice.js
new file mode 100644
index 0000000000..46a5dd87f0
--- /dev/null
+++ b/frontend/src/AppBuilder/_stores/slices/librarySlice.js
@@ -0,0 +1,13 @@
+export const createLibrarySlice = (set, get) => ({
+ jsLibraryRegistry: {},
+ jsLibraryLoading: false,
+ jsLibraryError: null,
+
+ setJsLibraryRegistry: (registry) => set(() => ({ jsLibraryRegistry: registry }), false, 'setJsLibraryRegistry'),
+
+ setJsLibraryLoading: (loading) => set(() => ({ jsLibraryLoading: loading }), false, 'setJsLibraryLoading'),
+
+ setJsLibraryError: (error) => set(() => ({ jsLibraryError: error }), false, 'setJsLibraryError'),
+
+ getJsLibraryRegistry: () => get().jsLibraryRegistry,
+});
diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js
index 3e83c3ca63..fc1ece5a73 100644
--- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js
+++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js
@@ -748,6 +748,12 @@ export const createQueryPanelSlice = (set, get) => ({
const { error } = e;
const errorMessage = typeof error === 'string' ? error : error?.message || 'Unknown error';
if (mode !== 'view') toast.error(errorMessage);
+ const result = handleFailure({
+ status: 'failed',
+ message: errorMessage,
+ data: e?.data || {},
+ description: errorMessage,
+ });
resolve({ status: 'failed', message: errorMessage });
});
});
@@ -1220,6 +1226,9 @@ export const createQueryPanelSlice = (set, get) => ({
const proxiedPage = deepClone(currentState?.page);
const proxiedQueriesInResolvedState = queriesInResolvedState;
+ const hasJsLibrariesAccess = get().license?.featureAccess?.appJsLibraries;
+ const libraryRegistry = hasJsLibrariesAccess ? get().jsLibraryRegistry || {} : {};
+
const evalFunction = Function(
[
'data',
@@ -1233,6 +1242,7 @@ export const createQueryPanelSlice = (set, get) => ({
'constants',
...(appType === 'module' ? ['input'] : []),
'actions',
+ ...Object.keys(libraryRegistry),
],
transformation
);
@@ -1258,7 +1268,8 @@ export const createQueryPanelSlice = (set, get) => ({
log: function (log) {
return actions.log.call(actions, log, true);
},
- }
+ },
+ ...Object.values(libraryRegistry)
);
} catch (err) {
const stackLines = err.stack.split('\n');
@@ -1536,6 +1547,8 @@ export const createQueryPanelSlice = (set, get) => ({
try {
const AsyncFunction = new Function(`return Object.getPrototypeOf(async function(){}).constructor`)();
+ const hasJsLibrariesAccess = get().license?.featureAccess?.appJsLibraries;
+ const libraryRegistry = hasJsLibrariesAccess ? get().jsLibraryRegistry || {} : {};
const fnParams = [
'moment',
'_',
@@ -1549,6 +1562,7 @@ export const createQueryPanelSlice = (set, get) => ({
'constants',
...(!_.isEmpty(formattedParams) ? ['parameters'] : []), // Parameters are supported if builder has added atleast one parameter to the query
...(appType === 'module' ? ['input'] : []), // Include 'input' only for module,
+ ...Object.keys(libraryRegistry),
code,
];
var evalFn = new AsyncFunction(...fnParams);
@@ -1566,6 +1580,7 @@ export const createQueryPanelSlice = (set, get) => ({
resolvedState?.constants,
...(!_.isEmpty(formattedParams) ? [formattedParams] : []), // Parameters are supported if builder has added atleast one parameter to the query
...(appType === 'module' ? [resolvedState.input] : []), // Include 'input' only for module
+ ...Object.values(libraryRegistry),
];
result = {
status: 'ok',
diff --git a/frontend/src/AppBuilder/_stores/store.js b/frontend/src/AppBuilder/_stores/store.js
index e4b34dd3b4..e4b77f46c6 100644
--- a/frontend/src/AppBuilder/_stores/store.js
+++ b/frontend/src/AppBuilder/_stores/store.js
@@ -33,6 +33,7 @@ import { createWhiteLabellingSlice } from './slices/whiteLabellingSlice';
import { createFormComponentSlice } from './slices/componentSlices/formComponentSlice';
import { createInspectorSlice } from './slices/inspectorSlice';
import { createModuleSlice } from './slices/moduleSlice';
+import { createLibrarySlice } from './slices/librarySlice';
import { createDataQueryFolderSlice } from './slices/dataQueryFolderSlice';
import { listViewComponentSlice } from './slices/componentSlices/listViewComponentSlice';
import { createBranchSlice } from './slices/branchSlice';
@@ -71,6 +72,7 @@ export default create(
...createWhiteLabellingSlice(...state),
...createInspectorSlice(...state),
...createModuleSlice(...state),
+ ...createLibrarySlice(...state),
...createDataQueryFolderSlice(...state),
// component slices
...createFormComponentSlice(...state),
diff --git a/plugins/packages/oracledb/lib/manifest.json b/plugins/packages/oracledb/lib/manifest.json
index bd30e3e203..c95365c5a3 100644
--- a/plugins/packages/oracledb/lib/manifest.json
+++ b/plugins/packages/oracledb/lib/manifest.json
@@ -79,7 +79,7 @@
"default": "no",
"list": [
{
- "name": "Basic Connection (No TNS/Wallet)",
+ "name": "Basic Connection",
"value": "no"
},
{
diff --git a/server/src/modules/licensing/configs/LicenseBase.ts b/server/src/modules/licensing/configs/LicenseBase.ts
index a9dba47d58..cdbcacf7a9 100644
--- a/server/src/modules/licensing/configs/LicenseBase.ts
+++ b/server/src/modules/licensing/configs/LicenseBase.ts
@@ -597,6 +597,7 @@ export default class LicenseBase {
scim: this.scim,
observabilityEnabled: this.observabilityEnabled,
appHistory: this.appHistory,
+ appJsLibraries: this.appJsLibraries,
queryFolders: this.queryFolders,
workspaceEnv: this.workspaceEnv,
aiPlan: this.aiPlan,
@@ -639,6 +640,7 @@ export default class LicenseBase {
workflows: this.workflows,
startDate: this.startDate,
appHistoryEnabled: this.appHistory,
+ appJsLibrariesEnabled: this.appJsLibraries,
};
}
@@ -711,4 +713,15 @@ export default class LicenseBase {
}
return this._isEnvMapping;
}
+
+ public get appJsLibraries(): boolean {
+ if (this.IsBasicPlan) {
+ return !!this.BASIC_PLAN_TERMS.app?.features?.jsLibraries;
+ }
+
+ if (this._app?.features?.jsLibraries === undefined) {
+ return false;
+ }
+ return !!this._app?.features?.jsLibraries;
+ }
}
diff --git a/server/src/modules/licensing/constants/PlanTerms.ts b/server/src/modules/licensing/constants/PlanTerms.ts
index c1ad06fd0f..c46751c430 100644
--- a/server/src/modules/licensing/constants/PlanTerms.ts
+++ b/server/src/modules/licensing/constants/PlanTerms.ts
@@ -67,6 +67,7 @@ export const BASIC_PLAN_TERMS: Partial = {
promote: false,
release: false,
history: false,
+ jsLibraries: false,
},
components: {
navigation: false,
diff --git a/server/src/modules/licensing/constants/index.ts b/server/src/modules/licensing/constants/index.ts
index eb7f6f5ab8..82da51ad78 100644
--- a/server/src/modules/licensing/constants/index.ts
+++ b/server/src/modules/licensing/constants/index.ts
@@ -125,6 +125,7 @@ export enum LICENSE_FIELD {
AI_PLAN = 'aiPlan',
EXTERNAL_API = 'externalApiEnabled',
APP_HISTORY = 'appHistoryEnabled',
+ APP_JS_LIBRARIES = 'appJsLibrariesEnabled',
SCIM = 'scimEnabled',
PLAN = 'plan',
MODULES = 'modulesEnabled',
diff --git a/server/src/modules/licensing/helper.ts b/server/src/modules/licensing/helper.ts
index d843ec06c3..f727b477d8 100644
--- a/server/src/modules/licensing/helper.ts
+++ b/server/src/modules/licensing/helper.ts
@@ -172,6 +172,9 @@ export function getLicenseFieldValue(type: LICENSE_FIELD, licenseInstance: Licen
case LICENSE_FIELD.QUERY_FOLDERS:
return licenseInstance.queryFolders;
+ case LICENSE_FIELD.APP_JS_LIBRARIES:
+ return licenseInstance.appJsLibraries;
+
default:
return licenseInstance.terms;
}
diff --git a/server/src/modules/licensing/interfaces/terms.ts b/server/src/modules/licensing/interfaces/terms.ts
index bbde04201f..dcc9897d27 100644
--- a/server/src/modules/licensing/interfaces/terms.ts
+++ b/server/src/modules/licensing/interfaces/terms.ts
@@ -67,6 +67,7 @@ export interface Terms {
promote: boolean;
release: boolean;
history: boolean;
+ jsLibraries: boolean;
};
components?: {
navigation?: boolean;