maxLetters
+ ? `${children.substring(0, maxLetters)}...`
+ : children;
+
return (
= 92;
const isEdge = browser.name === 'Edge' && browser.major >= 92;
- const isSafari = browser.name === 'Safari' && browser.major >= 15 && browser.minor >= 4; // Handle minor version check for Safari
+ const isSafari = browser.name === 'Safari' && (browser.major > 15 || (browser.major === 15 && browser.minor >= 4));
const isFirefox = browser.name === 'Firefox' && browser.major >= 90;
- // console.log('browser--', browser, isChrome || isEdge || isSafari || isFirefox);
-
return isChrome || isEdge || isSafari || isFirefox;
}
function getBrowserUserAgent(userAgent) {
var regexps = {
- Chrome: [/Chrome\/(\S+)/],
- Firefox: [/Firefox\/(\S+)/],
- MSIE: [/MSIE (\S+);/],
- Opera: [/Opera\/.*?Version\/(\S+)/ /* Opera 10 */, /Opera\/(\S+)/ /* Opera 9 and older */],
- Safari: [/Version\/(\S+).*?Safari\//],
- },
+ Chrome: [/Chrome\/(\S+)/],
+ Firefox: [/Firefox\/(\S+)/],
+ MSIE: [/MSIE (\S+);/],
+ Opera: [/Opera\/.*?Version\/(\S+)/ /* Opera 10 */, /Opera\/(\S+)/ /* Opera 9 and older */],
+ Safari: [/Version\/(\S+).*?Safari\//],
+ },
re,
m,
browser,
diff --git a/frontend/src/_helpers/editorHelpers.js b/frontend/src/_helpers/editorHelpers.js
index c3c18a6587..2a0ed161db 100644
--- a/frontend/src/_helpers/editorHelpers.js
+++ b/frontend/src/_helpers/editorHelpers.js
@@ -1,58 +1,60 @@
-import { Button } from '@/Editor/Components/Button';
-import { Image } from '@/Editor/Components/Image/Image';
-import { Text } from '@/Editor/Components/Text';
-import { Table } from '@/Editor/Components/Table/Table';
-import { TextInput } from '@/Editor/Components/TextInput';
-import { NumberInput } from '@/Editor/Components/NumberInput';
-import { Container } from '@/Editor/Components/Container';
-import { Tabs } from '@/Editor/Components/Tabs';
-import { RichTextEditor } from '@/Editor/Components/RichTextEditor';
-import { DropDown } from '@/Editor/Components/DropDown';
-import { DropdownV2 } from '@/Editor/Components/DropdownV2/DropdownV2';
-import { Checkbox } from '@/Editor/Components/Checkbox';
-import { Datepicker } from '@/Editor/Components/Datepicker';
-import { DaterangePicker } from '@/Editor/Components/DaterangePicker';
-import { Multiselect } from '@/Editor/Components/Multiselect';
-import { MultiselectV2 } from '@/Editor/Components/MultiselectV2/MultiselectV2';
-import { Modal } from '@/Editor/Components/Modal';
-import { Chart } from '@/Editor/Components/Chart';
-import { Map as MapComponent } from '@/Editor/Components/Map/Map';
-import { QrScanner } from '@/Editor/Components/QrScanner/QrScanner';
-import { ToggleSwitch } from '@/Editor/Components/Toggle';
-import { ToggleSwitchV2 } from '@/Editor/Components/ToggleV2';
+// import { Button } from '@/Editor/Components/Button';
+// import { Image } from '@/Editor/Components/Image/Image';
+// import { Text } from '@/Editor/Components/Text';
+// import { Table } from '@/Editor/Components/Table/Table';
+// import { TextInput } from '@/Editor/Components/TextInput';
+// import { NumberInput } from '@/Editor/Components/NumberInput';
+// import { TextArea } from '@/Editor/Components/TextArea';
-import { RadioButton } from '@/Editor/Components/RadioButton';
-import { StarRating } from '@/Editor/Components/StarRating';
-import { Divider } from '@/Editor/Components/Divider';
-import { FilePicker } from '@/Editor/Components/FilePicker';
-import { PasswordInput } from '@/Editor/Components/PasswordInput';
-import { Calendar } from '@/Editor/Components/Calendar';
-import { Listview } from '@/Editor/Components/Listview';
-import { IFrame } from '@/Editor/Components/IFrame';
-import { CodeEditor } from '@/Editor/Components/CodeEditor';
-import { Timer } from '@/Editor/Components/Timer';
-import { Statistics } from '@/Editor/Components/Statistics';
-import { Pagination } from '@/Editor/Components/Pagination';
-import { Tags } from '@/Editor/Components/Tags';
-import { Spinner } from '@/Editor/Components/Spinner';
-import { CircularProgressBar } from '@/Editor/Components/CirularProgressbar';
-import { RangeSlider } from '@/Editor/Components/RangeSlider';
-import { Timeline } from '@/Editor/Components/Timeline';
-import { SvgImage } from '@/Editor/Components/SvgImage';
-import { Html } from '@/Editor/Components/Html';
-import { ButtonGroup } from '@/Editor/Components/ButtonGroup';
-import { CustomComponent } from '@/Editor/Components/CustomComponent/CustomComponent';
-import { VerticalDivider } from '@/Editor/Components/VerticalDivider';
-import { ColorPicker } from '@/Editor/Components/ColorPicker';
-import { KanbanBoard } from '@/Editor/Components/KanbanBoard/KanbanBoard';
-import { Kanban } from '@/Editor/Components/Kanban/Kanban';
-import { Steps } from '@/Editor/Components/Steps';
-import { TreeSelect } from '@/Editor/Components/TreeSelect';
-import { Icon } from '@/Editor/Components/Icon';
-import { Link } from '@/Editor/Components/Link/Link';
-import { Form } from '@/Editor/Components/Form/Form';
-import { BoundedBox } from '@/Editor/Components/BoundedBox/BoundedBox';
-import { isPDFSupported } from '@/_helpers/appUtils';
+// import { Container } from '@/Editor/Components/Container';
+// import { Tabs } from '@/Editor/Components/Tabs';
+// import { RichTextEditor } from '@/Editor/Components/RichTextEditor';
+// import { DropDown } from '@/Editor/Components/DropDown';
+// import { DropdownV2 } from '@/Editor/Components/DropdownV2/DropdownV2';
+// import { Checkbox } from '@/Editor/Components/Checkbox';
+// import { Datepicker } from '@/Editor/Components/Datepicker';
+// import { DaterangePicker } from '@/Editor/Components/DaterangePicker';
+// import { Multiselect } from '@/Editor/Components/Multiselect';
+// import { MultiselectV2 } from '@/Editor/Components/MultiselectV2/MultiselectV2';
+// import { Modal } from '@/Editor/Components/Modal';
+// import { Chart } from '@/Editor/Components/Chart';
+// import { Map as MapComponent } from '@/Editor/Components/Map/Map';
+// import { QrScanner } from '@/Editor/Components/QrScanner/QrScanner';
+// import { ToggleSwitch } from '@/Editor/Components/Toggle';
+// import { ToggleSwitchV2 } from '@/Editor/Components/ToggleV2';
+
+// import { RadioButton } from '@/Editor/Components/RadioButton';
+// import { StarRating } from '@/Editor/Components/StarRating';
+// import { Divider } from '@/Editor/Components/Divider';
+// import { FilePicker } from '@/Editor/Components/FilePicker';
+// import { PasswordInput } from '@/Editor/Components/PasswordInput';
+// import { Calendar } from '@/Editor/Components/Calendar';
+// import { Listview } from '@/Editor/Components/Listview';
+// import { IFrame } from '@/Editor/Components/IFrame';
+// import { CodeEditor } from '@/Editor/Components/CodeEditor';
+// import { Timer } from '@/Editor/Components/Timer';
+// import { Statistics } from '@/Editor/Components/Statistics';
+// import { Pagination } from '@/Editor/Components/Pagination';
+// import { Tags } from '@/Editor/Components/Tags';
+// import { Spinner } from '@/Editor/Components/Spinner';
+// import { CircularProgressBar } from '@/Editor/Components/CirularProgressbar';
+// import { RangeSlider } from '@/Editor/Components/RangeSlider';
+// import { Timeline } from '@/Editor/Components/Timeline';
+// import { SvgImage } from '@/Editor/Components/SvgImage';
+// import { Html } from '@/Editor/Components/Html';
+// import { ButtonGroup } from '@/Editor/Components/ButtonGroup';
+// import { CustomComponent } from '@/Editor/Components/CustomComponent/CustomComponent';
+// import { VerticalDivider } from '@/Editor/Components/verticalDivider';
+// import { ColorPicker } from '@/Editor/Components/ColorPicker';
+// import { KanbanBoard } from '@/Editor/Components/KanbanBoard/KanbanBoard';
+// import { Kanban } from '@/Editor/Components/Kanban/Kanban';
+// import { Steps } from '@/Editor/Components/Steps';
+// import { TreeSelect } from '@/Editor/Components/TreeSelect';
+// import { Icon } from '@/Editor/Components/Icon';
+// import { Link } from '@/Editor/Components/Link';
+// import { Form } from '@/Editor/Components/Form/Form';
+// import { BoundedBox } from '@/Editor/Components/BoundedBox/BoundedBox';
+// import { isPDFSupported } from '@/_helpers/appUtils';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import { useEditorStore } from '@/_stores/editorStore';
import './requestIdleCallbackPolyfill';
@@ -73,70 +75,71 @@ export function memoizeFunction(func) {
};
}
-export const AllComponents = {
- Button,
- Image,
- Text,
- TextInput,
- NumberInput,
- Table,
- Container,
- Tabs,
- RichTextEditor,
- DropDown,
- DropdownV2,
- Checkbox,
- Datepicker,
- DaterangePicker,
- Multiselect,
- MultiselectV2,
- Modal,
- Chart,
- Map: MapComponent,
- QrScanner,
- ToggleSwitch,
- RadioButton,
- StarRating,
- Divider,
- FilePicker,
- PasswordInput,
- Calendar,
- IFrame,
- CodeEditor,
- Listview,
- Timer,
- Statistics,
- Pagination,
- Tags,
- Spinner,
- CircularProgressBar,
- RangeSlider,
- Timeline,
- SvgImage,
- Html,
- ButtonGroup,
- CustomComponent,
- VerticalDivider,
- ColorPicker,
- KanbanBoard,
- Kanban,
- Steps,
- TreeSelect,
- Link,
- Icon,
- Form,
- BoundedBox,
- ToggleSwitchV2,
-};
-if (isPDFSupported()) {
- AllComponents.PDF = await import('@/Editor/Components/PDF').then((module) => module.PDF);
-}
+// export const AllComponents = {
+// Button,
+// Image,
+// Text,
+// TextInput,
+// NumberInput,
+// Table,
+// TextArea,
+// Container,
+// Tabs,
+// RichTextEditor,
+// DropDown,
+// DropdownV2,
+// Checkbox,
+// Datepicker,
+// DaterangePicker,
+// Multiselect,
+// MultiselectV2,
+// Modal,
+// Chart,
+// Map: MapComponent,
+// QrScanner,
+// ToggleSwitch,
+// RadioButton,
+// StarRating,
+// Divider,
+// FilePicker,
+// PasswordInput,
+// Calendar,
+// IFrame,
+// CodeEditor,
+// Listview,
+// Timer,
+// Statistics,
+// Pagination,
+// Tags,
+// Spinner,
+// CircularProgressBar,
+// RangeSlider,
+// Timeline,
+// SvgImage,
+// Html,
+// ButtonGroup,
+// CustomComponent,
+// VerticalDivider,
+// ColorPicker,
+// KanbanBoard,
+// Kanban,
+// Steps,
+// TreeSelect,
+// Link,
+// Icon,
+// Form,
+// BoundedBox,
+// ToggleSwitchV2,
+// };
+// if (isPDFSupported()) {
+// AllComponents.PDF = await import('@/Editor/Components/PDF').then((module) => module.PDF);
+// }
-export const getComponentToRender = (componentName) => {
- const shouldHideWidget = componentName === 'PDF' && !isPDFSupported();
- if (shouldHideWidget) return null;
- return AllComponents[componentName];
-};
+// export const getComponentToRender = (componentName) => {
+// const shouldHideWidget = componentName === 'PDF' && !isPDFSupported();
+// if (shouldHideWidget) return null;
+// return AllComponents[componentName];
+// };
export function isOnlyLayoutUpdate(diffState) {
const componentDiff = Object.keys(diffState).filter((key) => diffState[key]?.layouts && !diffState[key]?.component);
diff --git a/frontend/src/_helpers/routes.js b/frontend/src/_helpers/routes.js
index 0f552cb955..495ca04e18 100644
--- a/frontend/src/_helpers/routes.js
+++ b/frontend/src/_helpers/routes.js
@@ -21,6 +21,7 @@ export const getPrivateRoute = (page, params = {}) => {
workflows: '/workflows',
workspace_constants: '/workspace-constants',
profile_settings: '/profile-settings',
+ modules: '/modules',
};
let url = routes[page];
diff --git a/frontend/src/_services/apps.service.js b/frontend/src/_services/apps.service.js
index 82cf4312e9..c29dea481a 100644
--- a/frontend/src/_services/apps.service.js
+++ b/frontend/src/_services/apps.service.js
@@ -67,18 +67,30 @@ function getAll(page, folder, searchKey, type = 'front-end') {
}
function createApp(body = {}) {
- if (body.type === 'workflow') {
- return createWorkflow(body);
+ const requestOptions = {
+ method: 'POST',
+ headers: authHeader(),
+ credentials: 'include',
+ body: JSON.stringify(body),
+ };
+ switch (body.type) {
+ case 'workflow':
+ return createWorkflow(requestOptions);
+ case 'module':
+ return createModule(requestOptions);
+ default:
+ return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse);
}
- const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
- return fetch(`${config.apiUrl}/apps`, requestOptions).then(handleResponse);
}
-function createWorkflow(body = {}) {
- const requestOptions = { method: 'POST', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
+function createWorkflow(requestOptions) {
return fetch(`${config.apiUrl}/workflows`, requestOptions).then(handleResponse);
}
+function createModule(requestOptions) {
+ return fetch(`${config.apiUrl}/modules`, requestOptions).then(handleResponse);
+}
+
function cloneApp(id, name) {
const requestOptions = {
method: 'POST',
diff --git a/frontend/src/_styles/components.scss b/frontend/src/_styles/components.scss
index f76026c2b7..cbe60f8621 100644
--- a/frontend/src/_styles/components.scss
+++ b/frontend/src/_styles/components.scss
@@ -102,10 +102,7 @@ $btn-dark-color: #FFFFFF;
}
.leftsidebar-panel-header {
- background-color: var(--slate3);
- padding: 12px 16px;
- min-height: 52px;
- border-bottom: 1px solid var(--slate5);
+ padding: 12px 16px 0px 16px;
.panel-header-container {
@@ -132,6 +129,8 @@ $btn-dark-color: #FFFFFF;
.page-selector-panel-body {
padding: 4px;
border-right: 1px solid #DFE3E6;
+ padding-left:16px;
+ padding-right:16px;
&.dark-theme {
border-right: 1px solid var(--slate7);
@@ -249,10 +248,16 @@ $btn-dark-color: #FFFFFF;
display: flex;
align-items: baseline;
gap: 5px;
+ cursor: pointer !important;
+ pointer-events: unset !important;
&.disabled {
opacity: 1 !important;
}
+
+ svg {
+ margin-left: 5px;
+ }
}
}
diff --git a/frontend/src/_styles/left-sidebar.scss b/frontend/src/_styles/left-sidebar.scss
index e2fe92105a..6a0311b0c4 100644
--- a/frontend/src/_styles/left-sidebar.scss
+++ b/frontend/src/_styles/left-sidebar.scss
@@ -182,9 +182,17 @@
}
}
+.page {
+ .leftsidebar-panel-header {
+ margin-bottom: 8px;
+ }
+
+}
+
.debugger {
.leftsidebar-panel-header {
border-bottom: none;
+ margin-bottom: 8px;
}
.text-slate-12 {
@@ -207,7 +215,7 @@
}
.nav-item .nav-link {
- background-color: var(--slate3) !important;
+ background-color: var(--base) !important;
color: var(--slate11) !important;
}
@@ -859,4 +867,20 @@
height: calc(100% - 48px);
min-height: 300px;
position: relative;
-}
\ No newline at end of file
+}
+
+.left-sidebar-scrollbar {
+ &::-webkit-scrollbar {
+ width: 6px;
+ margin-right: 3px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: var(--interactive-default) !important;
+ border-radius: 3px;
+ }
+}
diff --git a/frontend/src/_styles/modules.scss b/frontend/src/_styles/modules.scss
new file mode 100644
index 0000000000..a812f949bb
--- /dev/null
+++ b/frontend/src/_styles/modules.scss
@@ -0,0 +1,75 @@
+.apps-modules-tabs {
+ .nav-link {
+ background-color: var(--page-default);
+ }
+
+ .nav-link.active {
+ border-bottom-width: medium !important;
+ }
+
+ li.nav-item {
+ flex: 1;
+
+ button {
+ width: 100%;
+ }
+ }
+
+}
+
+.apps-modules-navigation {
+ margin-bottom: 10px;
+
+ .tab-content {
+ display: none;
+ }
+}
+
+.apps-modules-tabs.dark-mode {
+ border-bottom-color: #2B394A;
+
+ .nav-link {
+ background-color: inherit;
+ }
+}
+
+#homePage-tab-front-end,
+#homePage-tab-module {
+ border-radius: 0;
+}
+
+.show-module-border {
+ outline: dotted 2px #CCD1D5 !important;
+}
+
+.module-container-canvas {
+ >div:first-child {
+ /* Firefox scrollbar support */
+ scrollbar-width: thin;
+ scrollbar-color: transparent transparent;
+ height: 100%;
+ overflow: hidden auto;
+
+ /* Hide scrollbar by default */
+ &::-webkit-scrollbar {
+ width: 6px;
+ background: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: transparent;
+ }
+
+ /* Show scrollbar only on hover */
+ &:hover {
+ &::-webkit-scrollbar-thumb {
+ background-color: #6a727c4d;
+ border-radius: 3px;
+ }
+ }
+
+ &:hover {
+ scrollbar-color: #6a727c4d transparent;
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss
index e9ed1ac9e9..b64a0cdc37 100644
--- a/frontend/src/_styles/theme.scss
+++ b/frontend/src/_styles/theme.scss
@@ -19,6 +19,7 @@
@import 'tailwindcss/utilities';
@import "./componentdesign.scss";
@import './pages-sidebar.scss';
+@import './modules.scss';
/* ibm-plex-sans-100 - latin */
@font-face {
@@ -806,6 +807,21 @@ button {
}
}
+ .viewer .main {
+ height: auto !important;
+
+ .canvas-container {
+ top: 0;
+ right: 0;
+ scrollbar-width: thin;
+ scrollbar-color: #6a727c4d transparent;
+
+ &::-webkit-scrollbar-thumb {
+ background-color: #6a727c4d !important;
+ }
+ }
+ }
+
@media screen and (max-height: 450px) {
.sidebar {
padding-top: 15px;
@@ -1552,6 +1568,13 @@ button {
border-top: 1px solid var(--slate5) !important;
}
+ &.module-editor-inspector {
+ .tab-content {
+ border-top: none !important;
+ }
+ }
+
+ /* Hide scrollbar for Chrome, Safari and Opera */
/* Hide scrollbar for Chrome, Safari and Opera */
.tab-content::-webkit-scrollbar {
display: none;
@@ -2707,6 +2730,14 @@ hr {
max-height: 10px;
z-index: 100;
min-width: 108px;
+
+ &.module-container {
+ .handle-content {
+ cursor: move;
+ color: #fff;
+ background: #c6cad0 !important;
+ }
+ }
}
@@ -4122,7 +4153,6 @@ input[type="text"] {
.rbc-event-label {
display: none;
}
-
}
.rbc-off-range-bg {
@@ -7581,6 +7611,13 @@ tbody {
.apploader {
height: 100vh;
+ &.module-mode {
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
.app-container {
height: 100%;
display: flex;
@@ -7942,6 +7979,11 @@ tbody {
display: grid;
grid-template-rows: auto 1fr auto;
+ // Added to work with AppTypeTab component
+ &:has(:nth-child(4):last-child) {
+ grid-template-rows: auto auto 1fr auto;
+ }
+
@media only screen and (max-width: 767px) {
display: none;
}
@@ -9144,7 +9186,8 @@ tbody {
}
.global-settings-app-wrapper {
- max-width: 190px;
+ max-width: 350px;
+ margin-right: 10px;
}
.version-manager-container {
@@ -10902,7 +10945,7 @@ tbody {
background: var(--indigo3);
border-radius: 6px;
padding: 5px 10px;
-
+
p {
font-weight: 500 !important;
line-height: 18px !important;
@@ -18907,3 +18950,69 @@ section.ai-message-prompt-input-wrapper {
max-height: 100px !important;
}
}
+
+.missing-groups-modal {
+ .modal-body {
+ padding: 16px;
+
+ .header {
+ padding-top: 12px;
+ font-weight: 500;
+ font-size: 14px;
+ }
+
+ .sub-header {
+ margin-bottom: 0px;
+ font-size: 12px;
+ }
+
+ .groups-list {
+ padding-top: 16px;
+ padding-bottom: 16px;
+
+ .container {
+ padding: 12px;
+ }
+ }
+
+ .info {
+ margin-bottom: 0px;
+ font-size: 12px;
+ padding-bottom: 24px;
+ }
+
+ .action-btns {
+ justify-content: space-between;
+ }
+
+ .primary-action,
+ .secondary-action {
+ padding: 8px !important;
+ font-size: 12px;
+ }
+
+ .toggle-button {
+ display: inline-flex;
+ align-items: center;
+ font-size: 14px;
+ color: var(--icon-brand);
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+ font-family: inherit;
+ }
+
+ .toggle-button:hover {
+ text-decoration: underline;
+ }
+
+ .toggle-button .chevron {
+ transition: transform 0.2s ease;
+ }
+
+ .toggle-button.expanded .chevron {
+ transform: rotate(180deg);
+ }
+ }
+}
diff --git a/frontend/src/_ui/Icon/solidIcons/Corners.jsx b/frontend/src/_ui/Icon/solidIcons/Corners.jsx
new file mode 100644
index 0000000000..72ca376981
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/Corners.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+const Corners = ({ style, fill = '#C1C8CD', width = '12', height = '13', className = '', viewBox = '0 0 12 13' }) => (
+
+
+
+
+
+);
+
+export default Corners;
diff --git a/frontend/src/_ui/Icon/solidIcons/EmptyStateModules.jsx b/frontend/src/_ui/Icon/solidIcons/EmptyStateModules.jsx
new file mode 100644
index 0000000000..963c4b7bef
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/EmptyStateModules.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+const EmptyStateModules = ({ fill = '#C1C8CD', width = '24', className = '', viewBox = '0 0 24 24' }) => (
+
+
+
+
+
+);
+
+export default EmptyStateModules;
diff --git a/frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx b/frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx
new file mode 100644
index 0000000000..eed8dd5e8c
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+
+const EnterpriseCrown = ({ fill = '#FCA23F', width = '12', className = '', viewBox = '0 0 16 16' }) => (
+
+
+
+);
+
+export default EnterpriseCrown;
diff --git a/frontend/src/_ui/Icon/solidIcons/FileCode.jsx b/frontend/src/_ui/Icon/solidIcons/FileCode.jsx
new file mode 100644
index 0000000000..ed1f3268c9
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/FileCode.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+const FileCode = ({ style, fill = '#C1C8CD', width = '25', className = '', viewBox = '0 0 12 12' }) => (
+
+
+
+
+
+);
+
+export default FileCode;
diff --git a/frontend/src/_ui/Icon/solidIcons/RemoveFolder.jsx b/frontend/src/_ui/Icon/solidIcons/RemoveFolder.jsx
new file mode 100644
index 0000000000..dd97454175
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/RemoveFolder.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+
+const RemoveFolder = ({ width = '14', fill = '#6A727C', className = '', viewBox = '0 0 14 14' }) => (
+
+
+
+
+
+
+
+
+
+
+);
+
+export default RemoveFolder;
diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js
index 2a7b801570..0e7192edaf 100644
--- a/frontend/src/_ui/Icon/solidIcons/index.js
+++ b/frontend/src/_ui/Icon/solidIcons/index.js
@@ -231,10 +231,15 @@ import CalendarSmall from './CalendarSmall.jsx';
import UserGroupsGrey from './UserGroupsGrey.jsx';
import AppLimitSvg from './AppLimitSvg.jsx';
import NewTabSmall from './NewTabSmall.jsx';
+import EmptyStateModules from './EmptyStateModules.jsx';
import Code from './Code.jsx';
import WorkflowV3 from './WorkflowV3.jsx';
import WorkspaceV3 from './WorkspaceV3.jsx';
+import EnterpriseCrown from './EnterrpiseCrown.jsx';
+import FileCode from './FileCode.jsx';
+import Corners from './Corners.jsx';
import Moon from './Moon.jsx';
+import RemoveFolder from './RemoveFolder.jsx';
const Icon = (props) => {
switch (props.name) {
@@ -356,6 +361,8 @@ const Icon = (props) => {
return ;
case 'enterprisev3':
return ;
+ case 'enterprisecrown':
+ return ;
case 'lockGradient':
return ;
case 'datasourceGradient':
@@ -370,6 +377,8 @@ const Icon = (props) => {
return ;
case 'expand':
return ;
+ case 'file-code':
+ return ;
case 'file01':
return ;
case 'filedownload':
@@ -496,6 +505,8 @@ const Icon = (props) => {
return ;
case 'remove01':
return ;
+ case 'removefolder':
+ return ;
case 'removerectangle':
return ;
case 'rightarrrow':
@@ -532,6 +543,8 @@ const Icon = (props) => {
return ;
case 'comments':
return ;
+ case 'corners':
+ return ;
case 'share':
return ;
case 'shield':
@@ -706,6 +719,8 @@ const Icon = (props) => {
return ;
case 'ai-crown':
return ;
+ case 'empty-state-modules':
+ return ;
case 'play01':
return ;
case 'moon':
diff --git a/frontend/src/_ui/Modal/index.jsx b/frontend/src/_ui/Modal/index.jsx
index b9d48f0d88..6ed7fa04db 100644
--- a/frontend/src/_ui/Modal/index.jsx
+++ b/frontend/src/_ui/Modal/index.jsx
@@ -18,6 +18,8 @@ export default function ModalBase({
className = '',
size = 'sm',
headerAction,
+ showHeader = true,
+ showFooter = true,
}) {
return (
-
-
- {title}
-
-
-
+ {showHeader && (
+
+
+ {title}
+
+
+
+ )}
{children ? (
children
@@ -45,28 +49,30 @@ export default function ModalBase({
)}
-
-
- Cancel
-
-
-
-
- {confirmBtnProps?.title || 'Continue'}
-
-
-
-
+ {showFooter && (
+
+
+ Cancel
+
+
+
+
+ {confirmBtnProps?.title || 'Continue'}
+
+
+
+
+ )}
);
}
diff --git a/frontend/src/components/ui/Input/CommonInput/TextInput.jsx b/frontend/src/components/ui/Input/CommonInput/TextInput.jsx
index 978e69798f..27903c783f 100644
--- a/frontend/src/components/ui/Input/CommonInput/TextInput.jsx
+++ b/frontend/src/components/ui/Input/CommonInput/TextInput.jsx
@@ -47,6 +47,7 @@ const TextInput = ({
diff --git a/frontend/src/components/ui/Input/Index.jsx b/frontend/src/components/ui/Input/Index.jsx
index 139019a198..dbb6806d6b 100644
--- a/frontend/src/components/ui/Input/Index.jsx
+++ b/frontend/src/components/ui/Input/Index.jsx
@@ -13,6 +13,7 @@ InputComponent.propTypes = {
type: PropTypes.oneOf(['text', 'number', 'editable title', 'password', 'email']),
value: PropTypes.string,
onChange: PropTypes.func,
+ onClear: PropTypes.func,
placeholder: PropTypes.string,
name: PropTypes.string,
id: PropTypes.string,
@@ -32,6 +33,7 @@ InputComponent.propTypes = {
InputComponent.defaultProps = {
type: 'text',
onChange: (e, validateObj) => {},
+ onClear: () => {},
placeholder: '',
name: '',
id: '',
diff --git a/frontend/src/modules/Modules/components/ModuleContainer/ModuleContainer.jsx b/frontend/src/modules/Modules/components/ModuleContainer/ModuleContainer.jsx
new file mode 100644
index 0000000000..8c18f534f6
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleContainer/ModuleContainer.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
+
+const ModuleContainer = () => {
+ return <>>;
+};
+
+export default withEditionSpecificComponent(ModuleContainer, 'Modules');
diff --git a/frontend/src/modules/Modules/components/ModuleContainer/index.js b/frontend/src/modules/Modules/components/ModuleContainer/index.js
new file mode 100644
index 0000000000..f5495b7be9
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleContainer/index.js
@@ -0,0 +1 @@
+export { default } from './ModuleContainer';
diff --git a/frontend/src/modules/Modules/components/ModuleContainerBlank/ModuleContainerBlank.jsx b/frontend/src/modules/Modules/components/ModuleContainerBlank/ModuleContainerBlank.jsx
new file mode 100644
index 0000000000..2ddb28f0d2
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleContainerBlank/ModuleContainerBlank.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
+
+const ModuleContainerBlank = () => {
+ return <>>;
+};
+
+export default withEditionSpecificComponent(ModuleContainerBlank, 'Modules');
diff --git a/frontend/src/modules/Modules/components/ModuleContainerBlank/index.js b/frontend/src/modules/Modules/components/ModuleContainerBlank/index.js
new file mode 100644
index 0000000000..b8954c974c
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleContainerBlank/index.js
@@ -0,0 +1 @@
+export { default } from './ModuleContainerBlank';
diff --git a/frontend/src/modules/Modules/components/ModuleContainerInspector/ModuleContainerInspector.jsx b/frontend/src/modules/Modules/components/ModuleContainerInspector/ModuleContainerInspector.jsx
new file mode 100644
index 0000000000..8782ae9ac7
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleContainerInspector/ModuleContainerInspector.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
+
+const ModuleContainerInspector = () => {
+ return <>>;
+};
+
+export default withEditionSpecificComponent(ModuleContainerInspector, 'Modules');
diff --git a/frontend/src/modules/Modules/components/ModuleContainerInspector/index.js b/frontend/src/modules/Modules/components/ModuleContainerInspector/index.js
new file mode 100644
index 0000000000..6417a2160e
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleContainerInspector/index.js
@@ -0,0 +1 @@
+export { default } from './ModuleContainerInspector';
diff --git a/frontend/src/modules/Modules/components/ModuleEditorBanner/ModuleEditorBanner.jsx b/frontend/src/modules/Modules/components/ModuleEditorBanner/ModuleEditorBanner.jsx
new file mode 100644
index 0000000000..3bf0d5b4e8
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleEditorBanner/ModuleEditorBanner.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
+
+const ModuleEditorBanner = () => {
+ return <>>;
+};
+
+export default withEditionSpecificComponent(ModuleEditorBanner, 'Modules');
diff --git a/frontend/src/modules/Modules/components/ModuleEditorBanner/index.js b/frontend/src/modules/Modules/components/ModuleEditorBanner/index.js
new file mode 100644
index 0000000000..c157c1e0b9
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleEditorBanner/index.js
@@ -0,0 +1 @@
+export { default } from './ModuleEditorBanner';
diff --git a/frontend/src/modules/Modules/components/ModuleManager/ModuleManager.jsx b/frontend/src/modules/Modules/components/ModuleManager/ModuleManager.jsx
new file mode 100644
index 0000000000..ee4d9a6a47
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleManager/ModuleManager.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
+
+const ModuleManager = () => {
+ return <>>;
+};
+
+export default withEditionSpecificComponent(ModuleManager, 'Modules');
diff --git a/frontend/src/modules/Modules/components/ModuleManager/index.js b/frontend/src/modules/Modules/components/ModuleManager/index.js
new file mode 100644
index 0000000000..5a89ccc37e
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleManager/index.js
@@ -0,0 +1 @@
+export { default } from './ModuleManager';
diff --git a/frontend/src/modules/Modules/components/ModuleViewer/ModuleViewer.jsx b/frontend/src/modules/Modules/components/ModuleViewer/ModuleViewer.jsx
new file mode 100644
index 0000000000..2297735672
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleViewer/ModuleViewer.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
+
+const ModuleViewer = () => {
+ return <>>;
+};
+
+export default withEditionSpecificComponent(ModuleViewer, 'Modules');
diff --git a/frontend/src/modules/Modules/components/ModuleViewer/index.js b/frontend/src/modules/Modules/components/ModuleViewer/index.js
new file mode 100644
index 0000000000..df8725725a
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleViewer/index.js
@@ -0,0 +1 @@
+export { default } from './ModuleViewer';
diff --git a/frontend/src/modules/Modules/components/ModuleViewerInspector/ModuleViewerInspector.jsx b/frontend/src/modules/Modules/components/ModuleViewerInspector/ModuleViewerInspector.jsx
new file mode 100644
index 0000000000..b5043e170a
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleViewerInspector/ModuleViewerInspector.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
+
+const ModuleViewerInspector = () => {
+ return <>>;
+};
+
+export default withEditionSpecificComponent(ModuleViewerInspector, 'Modules');
diff --git a/frontend/src/modules/Modules/components/ModuleViewerInspector/index.js b/frontend/src/modules/Modules/components/ModuleViewerInspector/index.js
new file mode 100644
index 0000000000..c95c5a7a46
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleViewerInspector/index.js
@@ -0,0 +1 @@
+export { default } from './ModuleViewerInspector';
diff --git a/frontend/src/modules/Modules/components/ModuleWidgetBox/ModuleWidgetBox.jsx b/frontend/src/modules/Modules/components/ModuleWidgetBox/ModuleWidgetBox.jsx
new file mode 100644
index 0000000000..7a43aa67e3
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleWidgetBox/ModuleWidgetBox.jsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
+
+const ModuleWidgetBox = () => {
+ return <>>;
+};
+
+export default withEditionSpecificComponent(ModuleWidgetBox, 'Modules');
diff --git a/frontend/src/modules/Modules/components/ModuleWidgetBox/index.js b/frontend/src/modules/Modules/components/ModuleWidgetBox/index.js
new file mode 100644
index 0000000000..1a903dadf7
--- /dev/null
+++ b/frontend/src/modules/Modules/components/ModuleWidgetBox/index.js
@@ -0,0 +1 @@
+export { default } from './ModuleWidgetBox';
diff --git a/frontend/src/modules/Modules/components/index.js b/frontend/src/modules/Modules/components/index.js
new file mode 100644
index 0000000000..0cd5f2301e
--- /dev/null
+++ b/frontend/src/modules/Modules/components/index.js
@@ -0,0 +1,19 @@
+import ModuleContainer from './ModuleContainer';
+import ModuleViewer from './ModuleViewer';
+import ModuleContainerInspector from './ModuleContainerInspector';
+import ModuleViewerInspector from './ModuleViewerInspector';
+import ModuleWidgetBox from './ModuleWidgetBox';
+import ModuleManager from './ModuleManager';
+import ModuleEditorBanner from './ModuleEditorBanner';
+import ModuleContainerBlank from './ModuleContainerBlank';
+
+export {
+ ModuleContainer,
+ ModuleViewer,
+ ModuleContainerInspector,
+ ModuleViewerInspector,
+ ModuleWidgetBox,
+ ModuleManager,
+ ModuleEditorBanner,
+ ModuleContainerBlank,
+};
diff --git a/frontend/src/modules/Modules/index.js b/frontend/src/modules/Modules/index.js
new file mode 100644
index 0000000000..04dab890ed
--- /dev/null
+++ b/frontend/src/modules/Modules/index.js
@@ -0,0 +1,2 @@
+const Modules = (props) => [];
+export default Modules;
diff --git a/frontend/src/modules/common/components/BaseAppActionModal/BaseAppActionModal.jsx b/frontend/src/modules/common/components/BaseAppActionModal/BaseAppActionModal.jsx
index 76b647f545..3d21041205 100644
--- a/frontend/src/modules/common/components/BaseAppActionModal/BaseAppActionModal.jsx
+++ b/frontend/src/modules/common/components/BaseAppActionModal/BaseAppActionModal.jsx
@@ -4,7 +4,7 @@ import { AppModal } from '@/_components';
const BaseAppActionModal = ({ configs, modalStates, ...props }) => {
const getActiveConfig = () => {
switch (true) {
- case modalStates.showCreateAppModal || modalStates.showCreateModuleModal:
+ case modalStates.showCreateAppModal:
return configs.create;
case modalStates.showCloneAppModal:
return configs.clone;
diff --git a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/PromoteVersionButton.jsx b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/PromoteVersionButton.jsx
index a0e14f7bb2..f30c40b82f 100644
--- a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/PromoteVersionButton.jsx
+++ b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/PromoteVersionButton.jsx
@@ -4,12 +4,14 @@ import { shallow } from 'zustand/shallow';
import { ToolTip } from '@/_components/ToolTip';
import { PromoteConfirmationModal } from './components';
import useStore from '@/AppBuilder/_stores/store';
+import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
const PromoteVersionButton = () => {
+ const { moduleId } = useModuleContext();
const [promoteModalData, setPromoteModalData] = useState(null);
const { isSaving, editingVersion, appVersionEnvironment, environments, selectedEnvironment, currentEnvIndex } = useStore(
(state) => ({
- isSaving: state.app.isSaving,
+ isSaving: state.appStore.modules[moduleId].app.isSaving,
editingVersion: state.currentVersionId,
selectedEnvironment: state.selectedEnvironment,
environments: state.environments,
diff --git a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/components/PromoteConfirmationModal/PromoteConfirmationModal.jsx b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/components/PromoteConfirmationModal/PromoteConfirmationModal.jsx
index cae938d130..cbc1284c06 100644
--- a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/components/PromoteConfirmationModal/PromoteConfirmationModal.jsx
+++ b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/PromoteVersionButton/components/PromoteConfirmationModal/PromoteConfirmationModal.jsx
@@ -9,8 +9,10 @@ import ArrowRightIcon from '@assets/images/icons/arrow-right.svg';
import '@/_styles/versions.scss';
import { shallow } from 'zustand/shallow';
import useStore from '@/AppBuilder/_stores/store';
+import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
const PromoteConfirmationModal = React.memo(({ data, onClose }) => {
+ const { moduleId } = useModuleContext();
const [promotingEnvironment, setPromotingEnvironment] = useState(false);
const darkMode = localStorage.getItem('darkMode') === 'true' || false;
const currentVersionId = useStore((state) => state.currentVersionId);
@@ -22,7 +24,7 @@ const PromoteConfirmationModal = React.memo(({ data, onClose }) => {
(state) => ({
promoteAppVersionAction: state.promoteAppVersionAction,
selectedVersion: state.selectedVersion,
- creationMode: state.app.creationMode,
+ creationMode: state.appStore.modules[moduleId].app.creationMode,
}),
shallow
);
diff --git a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/ReleaseVersionButton/ReleaseVersionButton.jsx b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/ReleaseVersionButton/ReleaseVersionButton.jsx
index 494eaa52e9..48b77cc238 100644
--- a/frontend/src/modules/common/components/BasePromoteReleaseButton/components/ReleaseVersionButton/ReleaseVersionButton.jsx
+++ b/frontend/src/modules/common/components/BasePromoteReleaseButton/components/ReleaseVersionButton/ReleaseVersionButton.jsx
@@ -8,8 +8,10 @@ import { shallow } from 'zustand/shallow';
import '@/_styles/versions.scss';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import useStore from '@/AppBuilder/_stores/store';
+import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext';
const ReleaseVersionButton = function DeployVersionButton() {
+ const { moduleId } = useModuleContext();
const [isReleasing, setIsReleasing] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
const { isVersionReleased, editingVersion, updateReleasedVersionId, appId, versionToBeReleased, name } = useStore(
@@ -19,7 +21,7 @@ const ReleaseVersionButton = function DeployVersionButton() {
editingVersion: state.editingVersion,
isEditorFreezed: state.isEditorFreezed,
updateReleasedVersionId: state.updateReleasedVersionId,
- appId: state.app.appId,
+ appId: state.appStore.modules[moduleId].app.appId,
versionToBeReleased: state.currentVersionId,
// selectedVersionId: state.selectedVersion.id,
}),
diff --git a/frontend/src/modules/dashboard/components/AppTypeTab/AppTypeTab.jsx b/frontend/src/modules/dashboard/components/AppTypeTab/AppTypeTab.jsx
new file mode 100644
index 0000000000..293d325c59
--- /dev/null
+++ b/frontend/src/modules/dashboard/components/AppTypeTab/AppTypeTab.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { withEditionSpecificComponent } from '@/modules/common/helpers/withEditionSpecificComponent';
+
+const AppTypeTab = () => {
+ return <>>;
+};
+export default withEditionSpecificComponent(AppTypeTab, 'Dashboard');
diff --git a/frontend/src/modules/dashboard/components/AppTypeTab/index.js b/frontend/src/modules/dashboard/components/AppTypeTab/index.js
new file mode 100644
index 0000000000..ae18e0c4c3
--- /dev/null
+++ b/frontend/src/modules/dashboard/components/AppTypeTab/index.js
@@ -0,0 +1 @@
+export { default } from './AppTypeTab';
diff --git a/frontend/src/modules/dashboard/components/index.js b/frontend/src/modules/dashboard/components/index.js
index ab540b5feb..d4ffff00c4 100644
--- a/frontend/src/modules/dashboard/components/index.js
+++ b/frontend/src/modules/dashboard/components/index.js
@@ -6,6 +6,7 @@ import SettingsMenu from './SettingsMenu';
import WorkspaceActions from './WorkspaceActions';
import ConsultationBanner from './ConsultationBanner';
import UserGroupMigrationBanner from './UserGroupMigrationBanner';
+import AppTypeTab from './AppTypeTab';
export {
ImportAppMenu,
@@ -16,4 +17,5 @@ export {
WorkspaceActions,
ConsultationBanner,
UserGroupMigrationBanner,
+ AppTypeTab,
};
diff --git a/frontend/src/modules/index.js b/frontend/src/modules/index.js
index 69786dee07..f6feaf45c0 100644
--- a/frontend/src/modules/index.js
+++ b/frontend/src/modules/index.js
@@ -13,6 +13,7 @@ import Settings from './Settings';
import Workflows from './workflows';
import WorkspaceSettings from './WorkspaceSettings';
import RenderWorkflow from './RenderWorkflow';
+import Modules from './Modules';
export {
onboarding,
@@ -27,4 +28,5 @@ export {
getAuditLogsRoutes,
RenderWorkflow,
AiBuilder,
+ Modules,
};
diff --git a/server/.version b/server/.version
index f982feb41b..f02113fe87 100644
--- a/server/.version
+++ b/server/.version
@@ -1 +1 @@
-3.14.0
+3.15.0
diff --git a/server/ee b/server/ee
index 876b886d1b..aff18e4808 160000
--- a/server/ee
+++ b/server/ee
@@ -1 +1 @@
-Subproject commit 876b886d1ba4f6c858984bda6fdcb9373e9bb01b
+Subproject commit aff18e48080fc533173d49b68e7ac397b46ae5b1
diff --git a/server/migrations/1744610362161-CreatePagePermissions.ts b/server/migrations/1744610362161-CreatePagePermissions.ts
index ca4afbac66..ebf622da8b 100644
--- a/server/migrations/1744610362161-CreatePagePermissions.ts
+++ b/server/migrations/1744610362161-CreatePagePermissions.ts
@@ -1,13 +1,7 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
-import { TOOLJET_EDITIONS } from '@modules/app/constants';
-import { getTooljetEdition } from '@helpers/utils.helper';
export class CreatePagePermissions1744610362161 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise
{
- if (getTooljetEdition() === TOOLJET_EDITIONS.CE) {
- return;
- }
-
await queryRunner.createTable(
new Table({
name: 'page_permissions',
diff --git a/server/migrations/1744611380594-CreatePageUsers.ts b/server/migrations/1744611380594-CreatePageUsers.ts
index 5fe4d126c7..f1c6c89beb 100644
--- a/server/migrations/1744611380594-CreatePageUsers.ts
+++ b/server/migrations/1744611380594-CreatePageUsers.ts
@@ -1,13 +1,7 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
-import { TOOLJET_EDITIONS } from '@modules/app/constants';
-import { getTooljetEdition } from '@helpers/utils.helper';
export class CreatePageUsers1744611380594 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise {
- if (getTooljetEdition() === TOOLJET_EDITIONS.CE) {
- return;
- }
-
await queryRunner.createTable(
new Table({
name: 'page_users',
diff --git a/server/src/dto/import-resources.dto.ts b/server/src/dto/import-resources.dto.ts
index f00992213e..89b3ee182c 100644
--- a/server/src/dto/import-resources.dto.ts
+++ b/server/src/dto/import-resources.dto.ts
@@ -1,4 +1,4 @@
-import { IsUUID, IsOptional, IsString, IsDefined, ValidateNested } from 'class-validator';
+import { IsUUID, IsOptional, IsString, IsDefined, ValidateNested, IsBoolean } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { ValidateTooljetDatabaseSchema } from './validators/tooljet-database.validator';
import { TjdbSchemaToLatestVersion } from './transformers/resource-transformer';
@@ -28,6 +28,10 @@ export class ImportResourcesDto {
// and instantiated data
@ValidateTooljetDatabaseSchema({ each: true })
tooljet_database: ImportTooljetDatabaseDto[];
+
+ @IsOptional()
+ @IsBoolean()
+ skip_page_permissions_group_check?: boolean;
}
export class ImportAppDto {
diff --git a/server/src/entities/group_permissions.entity.ts b/server/src/entities/group_permissions.entity.ts
index 92868d7510..693f4f930c 100644
--- a/server/src/entities/group_permissions.entity.ts
+++ b/server/src/entities/group_permissions.entity.ts
@@ -3,7 +3,6 @@ import {
Column,
CreateDateColumn,
Entity,
- Index,
JoinColumn,
ManyToOne,
OneToMany,
@@ -21,7 +20,6 @@ export class GroupPermissions extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
- @Index()
@Column({ name: 'organization_id', nullable: false })
organizationId: string;
diff --git a/server/src/entities/group_users.entity.ts b/server/src/entities/group_users.entity.ts
index 29771a5557..03ac55386b 100644
--- a/server/src/entities/group_users.entity.ts
+++ b/server/src/entities/group_users.entity.ts
@@ -3,7 +3,6 @@ import {
Column,
CreateDateColumn,
Entity,
- Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
@@ -17,11 +16,9 @@ export class GroupUsers extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
- @Index()
@Column({ name: 'user_id', nullable: false })
userId: string;
- @Index()
@Column({ name: 'group_id', nullable: false })
groupId: string;
diff --git a/server/src/entities/page_users.entity.ts b/server/src/entities/page_users.entity.ts
index ca3ef77c65..960be5b32f 100644
--- a/server/src/entities/page_users.entity.ts
+++ b/server/src/entities/page_users.entity.ts
@@ -1,4 +1,4 @@
-import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, Index } from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm';
import { User } from './user.entity';
import { PagePermission } from './page_permissions.entity';
import { GroupPermissions } from './group_permissions.entity';
@@ -8,15 +8,12 @@ export class PageUser {
@PrimaryGeneratedColumn('uuid')
id: string;
- @Index()
@Column({ name: 'page_permissions_id', type: 'uuid' })
pagePermissionsId: string;
- @Index()
@Column({ name: 'user_id', type: 'uuid', nullable: true })
userId: string | null;
- @Index()
@Column({ name: 'permission_groups_id', type: 'uuid', nullable: true })
permissionGroupsId: string | null;
diff --git a/server/src/helpers/error_type.constant.ts b/server/src/helpers/error_type.constant.ts
index 4b968f002e..cb483e8896 100644
--- a/server/src/helpers/error_type.constant.ts
+++ b/server/src/helpers/error_type.constant.ts
@@ -1,5 +1,7 @@
export const APP_ERROR_TYPE = {
IMPORT_EXPORT_SERVICE: {
UNSUPPORTED_VERSION_ERROR: 'Apps built on later versions of ToolJet cannot be imported',
+ PAGE_PERMISSION_GROUP_ERROR: 'Following groups are missing from the workspace',
+ PERMISSION_CHECK: 'permission-check',
},
};
diff --git a/server/src/modules/app/constants/modules.ts b/server/src/modules/app/constants/modules.ts
index 7558629c3a..95fe1fde9a 100644
--- a/server/src/modules/app/constants/modules.ts
+++ b/server/src/modules/app/constants/modules.ts
@@ -39,5 +39,6 @@ export enum MODULES {
APP_PERMISSIONS = 'AppPermissions',
AUDIT_LOGS = 'auditLogs',
EXTERNAL_APIS = 'externalApis',
+ MODULES = 'Modules',
GIT_SYNC = 'GitSync', //register
}
diff --git a/server/src/modules/app/module.ts b/server/src/modules/app/module.ts
index 332b02f8b3..808ec19f81 100644
--- a/server/src/modules/app/module.ts
+++ b/server/src/modules/app/module.ts
@@ -39,6 +39,7 @@ import { TemplatesModule } from '@modules/templates/module';
import { ImportExportResourcesModule } from '@modules/import-export-resources/module';
import { TooljetDbModule } from '@modules/tooljet-db/module';
import { WorkflowsModule } from '@modules/workflows/module';
+import { ModulesModule } from '@modules/modules/module';
import { AiModule } from '@modules/ai/module';
import { CustomStylesModule } from '@modules/custom-styles/module';
import { AppPermissionsModule } from '@modules/app-permissions/module';
@@ -96,6 +97,7 @@ export class AppModule implements OnModuleInit {
await TemplatesModule.register(configs),
await TooljetDbModule.register(configs),
await WorkflowsModule.register(configs),
+ await ModulesModule.register(configs),
await AiModule.register(configs),
await CustomStylesModule.register(configs),
await AppPermissionsModule.register(configs),
diff --git a/server/src/modules/apps/constants/index.ts b/server/src/modules/apps/constants/index.ts
index 234950ca4d..81b7b8cae4 100644
--- a/server/src/modules/apps/constants/index.ts
+++ b/server/src/modules/apps/constants/index.ts
@@ -15,6 +15,7 @@ export enum FEATURE_KEY {
export enum APP_TYPES {
FRONT_END = 'front-end',
WORKFLOW = 'workflow',
+ MODULE = 'module',
}
export enum LayoutDimensionUnits {
diff --git a/server/src/modules/apps/module.ts b/server/src/modules/apps/module.ts
index 6565c17ed1..2a54c29326 100644
--- a/server/src/modules/apps/module.ts
+++ b/server/src/modules/apps/module.ts
@@ -39,15 +39,7 @@ export class AppsModule {
return {
module: AppsModule,
imports: [
- TypeOrmModule.forFeature([
- App,
- Page,
- EventHandler,
- Organization,
- Component,
- VersionRepository,
- RolesRepository,
- ]),
+ TypeOrmModule.forFeature([App, Page, EventHandler, Organization, Component, VersionRepository]),
await FolderAppsModule.register(configs),
await ThemesModule.register(configs),
await FoldersModule.register(configs),
diff --git a/server/src/modules/apps/service.ts b/server/src/modules/apps/service.ts
index 5e1e36acf2..6d09dba04f 100644
--- a/server/src/modules/apps/service.ts
+++ b/server/src/modules/apps/service.ts
@@ -31,6 +31,7 @@ import { FoldersUtilService } from '@modules/folders/util.service';
import { FolderAppsUtilService } from '@modules/folder-apps/util.service';
import { PageService } from './services/page.service';
import { EventsService } from './services/event.service';
+import { ComponentsService } from './services/component.service';
import { LICENSE_FIELD } from '@modules/licensing/constants';
import { AppEnvironment } from '@entities/app_environments.entity';
import { OrganizationThemesUtilService } from '@modules/organization-themes/util.service';
@@ -38,6 +39,7 @@ import { IAppsService } from './interfaces/IService';
import { AiUtilService } from '@modules/ai/util.service';
import { RequestContext } from '@modules/request-context/service';
import { AUDIT_LOGS_REQUEST_CONTEXT_KEY } from '@modules/app/constants';
+import { MODULES } from '@modules/app/constants/modules';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
@@ -54,8 +56,9 @@ export class AppsService implements IAppsService {
protected readonly eventService: EventsService,
protected readonly organizationThemeUtilService: OrganizationThemesUtilService,
protected readonly aiUtilService: AiUtilService,
+ protected readonly componentsService: ComponentsService,
protected readonly eventEmitter: EventEmitter2
- ) {}
+ ) { }
async create(user: User, appCreateDto: AppCreateDto) {
const { name, icon, type } = appCreateDto;
return await dbTransactionWrap(async (manager: EntityManager) => {
@@ -100,8 +103,8 @@ export class AppsService implements IAppsService {
const version = versionId
? await this.versionRepository.findById(versionId, app.id)
: versionName
- ? await this.versionRepository.findByName(versionName, app.id)
- : // Handle version retrieval based on env
+ ? await this.versionRepository.findByName(versionName, app.id)
+ : // Handle version retrieval based on env
await this.versionRepository.findLatestVersionForEnvironment(
app.id,
envId,
@@ -205,6 +208,13 @@ export class AppsService implements IAppsService {
apps = await this.appsUtilService.all(user, parseInt(page || '1'), searchKey, type);
}
+ if (type === 'module') {
+ for (const app of apps) {
+ const appVersionId = app?.appVersions[0]?.id;
+ app.moduleContainer = await this.pageService.findModuleContainer(appVersionId);
+ }
+ }
+
const totalCount = await this.appsUtilService.count(user, searchKey, type);
const totalPageCount = folderId ? totalFolderCount : totalCount;
@@ -301,42 +311,53 @@ export class AppsService implements IAppsService {
}
async getBySlug(app: App, user: User): Promise {
- const versionToLoad = app.currentVersionId
- ? await this.versionRepository.findVersion(app.currentVersionId)
- : await this.versionRepository.findVersion(app.editingVersion?.id);
+ const prepareResponse = async (app) => {
+ const versionToLoad = app.currentVersionId
+ ? await this.versionRepository.findVersion(app.currentVersionId)
+ : await this.versionRepository.findVersion(app.editingVersion?.id);
- const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(versionToLoad.id) : [];
- const eventsForVersion = app.editingVersion ? await this.eventService.findEventsForVersion(versionToLoad.id) : [];
- const appTheme = await this.organizationThemeUtilService.getTheme(
- app.organizationId,
- versionToLoad?.globalSettings?.theme?.id
- );
+ const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(versionToLoad.id) : [];
+ const eventsForVersion = app.editingVersion ? await this.eventService.findEventsForVersion(versionToLoad.id) : [];
+ const appTheme = await this.organizationThemeUtilService.getTheme(
+ app.organizationId,
+ versionToLoad?.globalSettings?.theme?.id
+ );
- if (app?.isPublic && user) {
- RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
- userId: user.id,
- organizationId: user.organizationId,
- resourceId: app.id,
- resourceName: app.name,
- });
- }
+ if (app?.isPublic && user) {
+ RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
+ userId: user.id,
+ organizationId: user.organizationId,
+ resourceId: app.id,
+ resourceName: app.name,
+ resourceType: MODULES.APP,
+ });
+ }
- // serialize
- return {
- current_version_id: app['currentVersionId'],
- data_queries: versionToLoad?.dataQueries,
- definition: versionToLoad?.definition,
- is_public: app.isPublic,
- is_maintenance_on: app.isMaintenanceOn,
- name: app.name,
- slug: app.slug,
- events: eventsForVersion,
- pages: this.appsUtilService.mergeDefaultComponentData(pagesForVersion),
- homePageId: versionToLoad.homePageId,
- globalSettings: { ...versionToLoad.globalSettings, theme: appTheme },
- showViewerNavigation: versionToLoad.showViewerNavigation,
- pageSettings: versionToLoad?.pageSettings,
+ // serialize
+ return {
+ current_version_id: app['currentVersionId'],
+ data_queries: versionToLoad?.dataQueries,
+ definition: versionToLoad?.definition,
+ is_public: app.isPublic,
+ is_maintenance_on: app.isMaintenanceOn,
+ name: app.name,
+ slug: app.slug,
+ events: eventsForVersion,
+ pages: this.appsUtilService.mergeDefaultComponentData(pagesForVersion),
+ homePageId: versionToLoad.homePageId,
+ globalSettings: { ...versionToLoad.globalSettings, theme: appTheme },
+ showViewerNavigation: versionToLoad.showViewerNavigation,
+ pageSettings: versionToLoad?.pageSettings,
+ };
};
+
+ const response = await prepareResponse(app);
+
+ const modules = await this.appsUtilService.fetchModules(app, false, undefined);
+
+ response['modules'] = await Promise.all(modules.map((module) => prepareResponse(module)));
+
+ return response;
}
async release(app: App, user: User, versionReleaseDto: VersionReleaseDto) {
diff --git a/server/src/modules/apps/services/app-import-export.service.ts b/server/src/modules/apps/services/app-import-export.service.ts
index 16fa8289f6..5d6da2ea7d 100644
--- a/server/src/modules/apps/services/app-import-export.service.ts
+++ b/server/src/modules/apps/services/app-import-export.service.ts
@@ -1,4 +1,4 @@
-import { BadRequestException, Injectable } from '@nestjs/common';
+import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { isEmpty, set } from 'lodash';
import { App } from 'src/entities/app.entity';
import { AppEnvironment } from 'src/entities/app_environments.entity';
@@ -33,6 +33,11 @@ import { DataSourcesUtilService } from '@modules/data-sources/util.service';
import { DataSourcesRepository } from '@modules/data-sources/repository';
import { AppEnvironmentUtilService } from '@modules/app-environments/util.service';
import { ComponentsService } from './component.service';
+import { GroupPermissions } from '@entities/group_permissions.entity';
+import { APP_ERROR_TYPE } from '@helpers/error_type.constant';
+import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants';
+import { PagePermission } from '@entities/page_permissions.entity';
+import { PageUser } from '@entities/page_users.entity';
import { UsersUtilService } from '@modules/users/util.service';
interface AppResourceMappings {
defaultDataSourceIdMapping: Record;
@@ -186,13 +191,31 @@ export class AppImportExportService {
}
const pages = await manager
- .createQueryBuilder(Page, 'pages')
- .where('pages.appVersionId IN(:...versionId)', {
+ .createQueryBuilder(Page, 'page')
+ .leftJoinAndSelect('page.permissions', 'permission')
+ .leftJoinAndSelect('permission.users', 'pageUser')
+ .leftJoinAndSelect('pageUser.permissionGroup', 'permissionGroup')
+ .where('page.appVersionId IN(:...versionId)', {
versionId: appVersions.map((v) => v.id),
})
- .orderBy('pages.created_at', 'ASC')
+ .orderBy('page.created_at', 'ASC')
.getMany();
+ const pagesWithPermissionGroups = pages.map((page) => {
+ const groupPermission = page.permissions.find((perm) => perm.type === 'GROUP');
+
+ return {
+ ...page,
+ permissions: groupPermission
+ ? {
+ permissionGroup: groupPermission.users
+ .map((user) => user.permissionGroup?.name)
+ .filter((name): name is string => Boolean(name)),
+ }
+ : undefined,
+ };
+ });
+
const components =
pages.length > 0
? await manager
@@ -214,7 +237,7 @@ export class AppImportExportService {
.getMany();
appToExport['components'] = components;
- appToExport['pages'] = pages;
+ appToExport['pages'] = pagesWithPermissionGroups;
appToExport['events'] = events;
appToExport['dataQueries'] = dataQueries;
appToExport['dataSources'] = dataSources;
@@ -812,6 +835,10 @@ export class AppImportExportService {
});
}
+ if (page.permissions) {
+ pageCreated.permissions = page.permissions;
+ }
+
appResourceMappings.pagesMapping[page.id] = pageCreated.id;
isHomePage = importingAppVersion.homePageId === page.id;
@@ -820,6 +847,9 @@ export class AppImportExportService {
updateHomepageId = pageCreated.id;
}
+ //create page permissions of page if flag enabled in dto
+ await this.createPagePermissionsForGroups(pageCreated, user.organizationId, manager);
+
const pageComponents = importingComponents.filter((component) => component.pageId === page.id);
const newComponentIdsMap = {};
@@ -936,6 +966,7 @@ export class AppImportExportService {
});
}
}
+
// relink page groups
const updateArr = [];
for (const { pageId, groupId } of pageGroupIdArr) {
@@ -1327,6 +1358,76 @@ export class AppImportExportService {
return pageSettings;
}
+ async checkIfGroupPermissionsExist(pages, organizationId) {
+ const allGroupNames = new Set();
+
+ for (const page of pages) {
+ const groupNames = page.permissions?.permissionGroup || [];
+ for (const name of groupNames) {
+ allGroupNames.add(name);
+ }
+ }
+
+ if (!allGroupNames.size) return;
+
+ return await dbTransactionWrap(async (manager: EntityManager) => {
+ const existingGroups = await manager
+ .createQueryBuilder(GroupPermissions, 'gp')
+ .where('gp.name IN (:...names)', { names: Array.from(allGroupNames) })
+ .andWhere('gp.organizationId = :organizationId', { organizationId })
+ .select(['gp.name'])
+ .getMany();
+
+ const existingGroupNames = new Set(existingGroups.map((g) => g.name));
+
+ const missingGroups = Array.from(allGroupNames).filter((name) => !existingGroupNames.has(name));
+
+ if (missingGroups.length > 0) {
+ throw new HttpException(
+ {
+ message: { type: APP_ERROR_TYPE.IMPORT_EXPORT_SERVICE.PERMISSION_CHECK, data: missingGroups },
+ },
+ HttpStatus.BAD_REQUEST
+ );
+ }
+ });
+ }
+
+ async createPagePermissionsForGroups(page, organizationId: string, manager: EntityManager) {
+ const groupNames = page.permissions?.permissionGroup || [];
+ if (!groupNames.length) return;
+
+ const existingGroups = await manager
+ .createQueryBuilder(GroupPermissions, 'gp')
+ .where('gp.name IN (:...names)', { names: groupNames })
+ .andWhere('gp.organizationId = :organizationId', { organizationId })
+ .getMany();
+
+ const groupMap = new Map(existingGroups.map((g) => [g.name, g]));
+
+ // Filter to only existing group names
+ const validGroupNames = groupNames.filter((name) => groupMap.has(name));
+
+ // If no valid group names exist, do not create permissions
+ if (!validGroupNames.length) return;
+
+ const permission = manager.create(PagePermission, {
+ pageId: page.id,
+ type: PAGE_PERMISSION_TYPE.GROUP,
+ });
+
+ const savedPermission = await manager.save(permission);
+
+ const pageUsers = validGroupNames.map((name) =>
+ manager.create(PageUser, {
+ pagePermissionsId: savedPermission.id,
+ permissionGroupsId: groupMap.get(name).id,
+ })
+ );
+
+ await manager.save(pageUsers);
+ }
+
async createAppVersionsForImportedApp(
manager: EntityManager,
user: User,
diff --git a/server/src/modules/apps/services/component.service.ts b/server/src/modules/apps/services/component.service.ts
index fcc01e52f0..c6ef31f5b8 100644
--- a/server/src/modules/apps/services/component.service.ts
+++ b/server/src/modules/apps/services/component.service.ts
@@ -12,7 +12,7 @@ const _ = require('lodash');
@Injectable()
export class ComponentsService implements IComponentsService {
- constructor(protected eventHandlerService: EventsService) {}
+ constructor(protected eventHandlerService: EventsService) { }
findOne(id: string): Promise {
return dbTransactionWrap((manager: EntityManager) => {
@@ -97,6 +97,7 @@ export class ComponentsService implements IComponentsService {
} else if (
(componentData.type === 'DropdownV2' ||
componentData.type === 'MultiselectV2' ||
+ componentData.type === 'ModuleContainer' ||
componentData.type === 'Steps') &&
_.isArray(objValue)
) {
diff --git a/server/src/modules/apps/services/page.service.ts b/server/src/modules/apps/services/page.service.ts
index fa0b2864e0..02bea48dab 100644
--- a/server/src/modules/apps/services/page.service.ts
+++ b/server/src/modules/apps/services/page.service.ts
@@ -304,4 +304,8 @@ export class PageService implements IPageService {
return await this.pageHelperService.rearrangePagesOrderPostDeletion(pageExists, manager);
}, appVersionId);
}
+
+ async findModuleContainer(appVersionId: string): Promise {
+ return this.pageHelperService.findModuleContainer(appVersionId);
+ }
}
diff --git a/server/src/modules/apps/services/page.util.service.ts b/server/src/modules/apps/services/page.util.service.ts
index cb10863972..aface2cc16 100644
--- a/server/src/modules/apps/services/page.util.service.ts
+++ b/server/src/modules/apps/services/page.util.service.ts
@@ -79,4 +79,8 @@ export class PageHelperService implements IPageHelperService {
page.index = dto.index;
return page;
}
+
+ public async findModuleContainer(appVersionId: string): Promise {
+ return null;
+ }
}
diff --git a/server/src/modules/apps/services/widget-config/index.js b/server/src/modules/apps/services/widget-config/index.js
index fdb4cf26df..c1ac0d205b 100644
--- a/server/src/modules/apps/services/widget-config/index.js
+++ b/server/src/modules/apps/services/widget-config/index.js
@@ -58,6 +58,8 @@ import { kanbanBoardConfig } from './kanbanBoard';
import { datetimePickerV2Config } from './datetimepickerV2';
import { datePickerV2Config } from './datepickerV2';
import { timePickerConfig } from './timepicker';
+import { moduleContainerConfig } from './moduleContainer';
+import { moduleViewerConfig } from './moduleViewer';
import { emailinputConfig } from './emailinput';
import { phoneinputConfig } from './phoneinput';
import {currencyinputConfig} from './currencyinput';
@@ -126,6 +128,8 @@ const widgets = {
linkConfig,
iconConfig,
boundedBoxConfig,
+ moduleContainerConfig,
+ moduleViewerConfig
};
const universalProps = {
diff --git a/server/src/modules/apps/services/widget-config/moduleContainer.js b/server/src/modules/apps/services/widget-config/moduleContainer.js
new file mode 100644
index 0000000000..af0f77c823
--- /dev/null
+++ b/server/src/modules/apps/services/widget-config/moduleContainer.js
@@ -0,0 +1,36 @@
+export const moduleContainerConfig = {
+ name: 'ModuleContainer',
+ displayName: 'Module Container',
+ description: 'Module Container',
+ component: 'ModuleContainer',
+ defaultSize: {
+ width: 10,
+ height: 400,
+ },
+ others: {
+ showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
+ showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
+ },
+ properties: {
+ inputItems: { type: 'array', displayName: 'Input' },
+ outputItems: { type: 'array', displayName: 'Output' },
+ },
+ events: {},
+ styles: {},
+ exposedVariables: {},
+ actions: [],
+ definition: {
+ others: {
+ showOnDesktop: { value: '{{true}}' },
+ showOnMobile: { value: '{{false}}' },
+ },
+ properties: {
+ inputItems: { value: [] },
+ outputItems: { value: [] },
+ },
+ events: [],
+ styles: {
+ backgroundColor: { value: '#fff' },
+ },
+ },
+};
diff --git a/server/src/modules/apps/services/widget-config/moduleViewer.js b/server/src/modules/apps/services/widget-config/moduleViewer.js
new file mode 100644
index 0000000000..b0f5342787
--- /dev/null
+++ b/server/src/modules/apps/services/widget-config/moduleViewer.js
@@ -0,0 +1,31 @@
+export const moduleViewerConfig = {
+ name: 'ModuleViewer',
+ displayName: 'Module',
+ description: 'Module',
+ component: 'ModuleViewer',
+ defaultSize: {
+ width: 10,
+ height: 400,
+ },
+ others: {
+ showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
+ showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
+ },
+ properties: {},
+ events: {},
+ styles: {},
+ exposedVariables: {},
+ actions: [],
+ definition: {
+ others: {
+ showOnDesktop: { value: '{{true}}' },
+ showOnMobile: { value: '{{false}}' },
+ },
+ properties: {},
+ events: [],
+ styles: {
+ backgroundColor: { value: '#fff' },
+ },
+ },
+ };
+
\ No newline at end of file
diff --git a/server/src/modules/apps/util.service.ts b/server/src/modules/apps/util.service.ts
index 68f76c65b4..e90e98a467 100644
--- a/server/src/modules/apps/util.service.ts
+++ b/server/src/modules/apps/util.service.ts
@@ -38,6 +38,8 @@ import { DataSourcesRepository } from '@modules/data-sources/repository';
import { IAppsUtilService } from './interfaces/IUtilService';
import { DataSourcesUtilService } from '@modules/data-sources/util.service';
import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
+import { Component } from 'src/entities/component.entity';
+import { Layout } from 'src/entities/layout.entity';
import { WorkspaceAppsResponseDto } from '@modules/external-apis/dto';
import { RequestContext } from '@modules/request-context/service';
import { RenameAppOrVersionDto } from '@modules/app-git/dto';
@@ -55,7 +57,7 @@ export class AppsUtilService implements IAppsUtilService {
protected readonly abilityService: AbilityService,
protected readonly dataSourceRepository: DataSourcesRepository,
protected readonly dataSourceUtilService: DataSourcesUtilService
- ) {}
+ ) { }
async create(name: string, user: User, type: string, manager: EntityManager): Promise {
return await dbTransactionWrap(async (manager: EntityManager) => {
const app = await catchDbException(() => {
@@ -95,8 +97,52 @@ export class AppsUtilService implements IAppsUtilService {
})
);
+ if (type === 'module') {
+ const moduleContainer = await manager.save(
+ manager.create(Component, {
+ name: 'ModuleContainer',
+ type: 'ModuleContainer',
+ pageId: defaultHomePage.id,
+ properties: {
+ inputItems: { value: [] },
+ outputItems: { value: [] },
+ visibility: { value: '{{true}}' },
+ },
+ styles: {
+ backgroundColor: { value: '#fff' },
+ },
+ displayPreferences: {
+ showOnDesktop: { value: '{{true}}' },
+ showOnMobile: { value: '{{true}}' },
+ },
+ })
+ );
+
+ await manager.save(
+ manager.create(Layout, {
+ component: moduleContainer,
+ type: 'desktop',
+ top: 50,
+ left: 6,
+ height: 400,
+ width: 38,
+ })
+ );
+
+ await manager.save(
+ manager.create(Layout, {
+ component: moduleContainer,
+ type: 'mobile',
+ top: 50,
+ left: 6,
+ height: 400,
+ width: 38,
+ })
+ );
+ }
+
// Set default values for app version
- appVersion.showViewerNavigation = true;
+ appVersion.showViewerNavigation = type === 'module' ? false : true;
appVersion.homePageId = defaultHomePage.id;
appVersion.globalSettings = {
hideHeader: false,
@@ -182,8 +228,8 @@ export class AppsUtilService implements IAppsUtilService {
const processEnvironmentName = environmentName
? environmentName
: !isMultiEnvironmentEnabled
- ? 'development'
- : null;
+ ? 'development'
+ : null;
const environment: AppEnvironment = environmentId
? await this.appEnvironmentUtilService.get(organizationId, environmentId)
@@ -399,20 +445,24 @@ export class AppsUtilService implements IAppsUtilService {
const viewableApps = userAppPermissions.hideAll
? [null, ...userAppPermissions.editableAppsId]
: [
- null,
- ...Array.from(
- new Set([
- ...userAppPermissions.editableAppsId,
- ...userAppPermissions.viewableAppsId.filter((id) => !userAppPermissions.hiddenAppsId.includes(id)),
- ])
- ),
- ];
+ null,
+ ...Array.from(
+ new Set([
+ ...userAppPermissions.editableAppsId,
+ ...userAppPermissions.viewableAppsId.filter((id) => !userAppPermissions.hiddenAppsId.includes(id)),
+ ])
+ ),
+ ];
const viewableAppsQb = manager
.createQueryBuilder(AppBase, 'viewable_apps')
.innerJoin('viewable_apps.user', 'user')
.addSelect(['user.firstName', 'user.lastName'])
.where('viewable_apps.organizationId = :organizationId', { organizationId: user.organizationId });
+ if (type === 'module') {
+ viewableAppsQb.leftJoinAndSelect('viewable_apps.appVersions', 'versions');
+ }
+
if (type) viewableAppsQb.andWhere('viewable_apps.type = :type', { type: type });
if (searchKey) {
@@ -529,6 +579,43 @@ export class AppsUtilService implements IAppsUtilService {
return components;
}
+ async fetchModules(app: App, allVersions: boolean = false, versionId: string): Promise {
+ const versionToLoad = versionId
+ ? await this.versionRepository.findVersion(versionId)
+ : app.currentVersionId
+ ? await this.versionRepository.findVersion(app.currentVersionId)
+ : await this.versionRepository.findVersion(app.editingVersion?.id);
+
+ const modules = await dbTransactionWrap(async (manager) => {
+ const moduleComponents = await manager
+ .createQueryBuilder(Component, 'component')
+ .leftJoinAndSelect(Page, 'page', 'page.id = component.page_id')
+ .leftJoinAndSelect(AppVersion, 'app_version', 'app_version.id = page.app_version_id')
+ .leftJoinAndSelect(App, 'app', 'app.id = app_version.app_id')
+ .andWhere(
+ `component.type = :module ${allVersions ? '' : 'AND app_version.id = :appVersionId'} AND app.id = :appId`,
+ {
+ module: 'ModuleViewer',
+ appVersionId: versionToLoad.id,
+ appId: app.id,
+ }
+ )
+ .getMany();
+
+ const moduleAppIds = moduleComponents.map((moduleComponent) => moduleComponent.properties.moduleAppId.value);
+
+ const modules =
+ moduleAppIds.length > 0
+ ? await manager
+ .createQueryBuilder(App, 'app')
+ .where('app.id IN (:...moduleAppIds)', { moduleAppIds })
+ .distinct(true)
+ .getMany()
+ : [];
+ return modules;
+ });
+ return modules;
+ }
async findAllOrganizationApps(organizationId: string): Promise {
return await this.appRepository.findAllOrganizationApps(organizationId);
}
diff --git a/server/src/modules/import-export-resources/service.ts b/server/src/modules/import-export-resources/service.ts
index e8292c8fd7..e47205258e 100644
--- a/server/src/modules/import-export-resources/service.ts
+++ b/server/src/modules/import-export-resources/service.ts
@@ -69,6 +69,18 @@ export class ImportExportResourcesService {
let tableNameMapping = {};
const imports = { app: [], tooljet_database: [], tableNameMapping: {} };
const importingVersion = importResourcesDto.tooljet_version;
+ const skipPagePermissionsGroupCheck = importResourcesDto.skip_page_permissions_group_check;
+
+ if (!isEmpty(importResourcesDto.app) && !skipPagePermissionsGroupCheck) {
+ for (const appImportDto of importResourcesDto.app) {
+ let appParams = appImportDto.definition;
+ if (appParams?.appV2) {
+ appParams = { ...appParams.appV2 };
+ const pages = appParams?.pages;
+ pages?.length && (await this.appImportExportService.checkIfGroupPermissionsExist(pages, user.organizationId));
+ }
+ }
+ }
if (!isEmpty(importResourcesDto.tooljet_database)) {
const res = await this.tooljetDbImportExportService.bulkImport(importResourcesDto, importingVersion, cloning);
diff --git a/server/src/modules/modules/IModulesController.ts b/server/src/modules/modules/IModulesController.ts
new file mode 100644
index 0000000000..cd84b9c534
--- /dev/null
+++ b/server/src/modules/modules/IModulesController.ts
@@ -0,0 +1,6 @@
+import { User } from '@entities/user.entity';
+import { AppCreateDto } from '@modules/apps/dto';
+
+export interface IModulesController {
+ create(user: User, appCreateDto: AppCreateDto): Promise;
+}
diff --git a/server/src/modules/modules/constants/index.ts b/server/src/modules/modules/constants/index.ts
new file mode 100644
index 0000000000..4adfa4a519
--- /dev/null
+++ b/server/src/modules/modules/constants/index.ts
@@ -0,0 +1,4 @@
+export enum FEATURE_KEY {
+ CREATE_MODULE = 'create_module',
+ GET_MODULES = 'get_modules',
+}
diff --git a/server/src/modules/modules/module.ts b/server/src/modules/modules/module.ts
new file mode 100644
index 0000000000..dc442ffa8c
--- /dev/null
+++ b/server/src/modules/modules/module.ts
@@ -0,0 +1,56 @@
+import { DynamicModule, Module } from '@nestjs/common';
+import { getImportPath } from '@modules/app/constants';
+import { ThemesModule } from '@modules/organization-themes/module';
+import { FoldersModule } from '@modules/folders/module';
+import { FolderAppsModule } from '@modules/folder-apps/module';
+import { OrganizationsModule } from '@modules/organizations/module';
+import { AppEnvironmentsModule } from '@modules/app-environments/module';
+import { OrganizationRepository } from '@modules/organizations/repository';
+import { DataSourcesRepository } from '@modules/data-sources/repository';
+import { VersionRepository } from '@modules/versions/repository';
+import { DataSourcesModule } from '@modules/data-sources/module';
+import { AiModule } from '@modules/ai/module';
+import { AppsRepository } from '@modules/apps/repository';
+import { AppPermissionsModule } from '@modules/app-permissions/module';
+import { RolesRepository } from '@modules/roles/repository';
+@Module({})
+export class ModulesModule {
+ static async register(configs: { IS_GET_CONTEXT: boolean }): Promise {
+ const importPath = await getImportPath(configs.IS_GET_CONTEXT);
+ const { ModulesController } = await import(`${importPath}/modules/modules.controller`);
+ const { AppsService } = await import(`${importPath}/apps/service`);
+ const { AppsUtilService } = await import(`${importPath}/apps/util.service`);
+ const { PageService } = await import(`${importPath}/apps/services/page.service`);
+ const { EventsService } = await import(`${importPath}/apps/services/event.service`);
+ const { ComponentsService } = await import(`${importPath}/apps/services/component.service`);
+ const { PageHelperService } = await import(`${importPath}/apps/services/page.util.service`);
+
+ return {
+ module: ModulesModule,
+ imports: [
+ await FolderAppsModule.register(configs),
+ await ThemesModule.register(configs),
+ await FoldersModule.register(configs),
+ await OrganizationsModule.register(configs),
+ await AppEnvironmentsModule.register(configs),
+ await DataSourcesModule.register(configs),
+ await AiModule.register(configs),
+ await AppPermissionsModule.register(configs),
+ ],
+ controllers: [ModulesController],
+ providers: [
+ AppsService,
+ VersionRepository,
+ AppsRepository,
+ PageService,
+ EventsService,
+ AppsUtilService,
+ ComponentsService,
+ PageHelperService,
+ OrganizationRepository,
+ DataSourcesRepository,
+ RolesRepository,
+ ],
+ };
+ }
+}
diff --git a/server/src/modules/modules/modules.controller.ts b/server/src/modules/modules/modules.controller.ts
new file mode 100644
index 0000000000..705d81dece
--- /dev/null
+++ b/server/src/modules/modules/modules.controller.ts
@@ -0,0 +1,20 @@
+import { Controller, Post, UseGuards, Body } from '@nestjs/common';
+import { User } from '@modules/app/decorators/user.decorator';
+import { JwtAuthGuard } from '@modules/session/guards/jwt-auth.guard';
+import { AppCreateDto } from '@modules/apps/dto';
+import { IModulesController } from '@modules/modules/IModulesController';
+import { InitModule } from '@modules/app/decorators/init-module';
+import { MODULES } from '@modules/app/constants/modules';
+import { InitFeature } from '@modules/app/decorators/init-feature.decorator';
+import { FEATURE_KEY } from '@modules/modules/constants';
+
+@InitModule(MODULES.MODULES)
+@Controller('modules')
+export class ModulesController implements IModulesController {
+ @InitFeature(FEATURE_KEY.CREATE_MODULE)
+ @UseGuards(JwtAuthGuard)
+ @Post()
+ async create(@User() user, @Body() appCreateDto: AppCreateDto): Promise {
+ throw new Error('Method not implemented.');
+ }
+}
diff --git a/server/src/modules/versions/module.ts b/server/src/modules/versions/module.ts
index 261401a18d..7fac84f033 100644
--- a/server/src/modules/versions/module.ts
+++ b/server/src/modules/versions/module.ts
@@ -9,6 +9,7 @@ import { DataSourcesModule } from '@modules/data-sources/module';
import { AppsRepository } from '@modules/apps/repository';
import { FeatureAbilityFactory } from './ability';
import { getImportPath } from '@modules/app/constants';
+import { AppPermissionsModule } from '@modules/app-permissions/module';
export class VersionModule {
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise {
@@ -33,6 +34,7 @@ export class VersionModule {
await DataSourcesModule.register(configs),
await AppEnvironmentsModule.register(configs),
await ThemesModule.register(configs),
+ await AppPermissionsModule.register(configs),
],
controllers: [ComponentsController, EventsController, PagesController, VersionController, VersionControllerV2],
providers: [
diff --git a/server/src/modules/versions/repository.ts b/server/src/modules/versions/repository.ts
index 91e71f5926..7bc12a4e19 100644
--- a/server/src/modules/versions/repository.ts
+++ b/server/src/modules/versions/repository.ts
@@ -164,6 +164,23 @@ export class VersionRepository extends Repository {
return appVersion.app;
}, manager || this.manager);
}
+
+ async findVersionsFromApp(app: App, manager?: EntityManager): Promise {
+ return dbTransactionWrap(async (manager: EntityManager) => {
+ const appVersions = await manager.find(AppVersion, {
+ where: { appId: app.id },
+ relations: [
+ 'app',
+ 'dataQueries',
+ 'dataQueries.dataSource',
+ 'dataQueries.plugins',
+ 'dataQueries.plugins.manifestFile',
+ ],
+ });
+ return appVersions;
+ }, manager || this.manager);
+ }
+
async getAppVersionById(versionId: string) {
return await dbTransactionWrap(async (manager: EntityManager) => {
const version = await manager.findOneOrFail(AppVersion, {
diff --git a/server/src/modules/versions/service.ts b/server/src/modules/versions/service.ts
index 1440401c61..2f936419cd 100644
--- a/server/src/modules/versions/service.ts
+++ b/server/src/modules/versions/service.ts
@@ -95,62 +95,78 @@ export class VersionService implements IVersionService {
}
async getVersion(app: App, user: User): Promise {
- const versionId = app.appVersions[0].id;
- const appVersion = await this.versionRepository.findVersion(versionId);
-
- const pagesForVersion = await this.pageService.findPagesForVersion(versionId);
- const eventsForVersion = await this.eventsService.findEventsForVersion(versionId);
-
- const appCurrentEditingVersion = JSON.parse(JSON.stringify(appVersion));
-
- if (
- appCurrentEditingVersion &&
- !(await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT))
- ) {
- const developmentEnv = await this.appEnvironmentUtilService.getByPriority(user.organizationId);
- appCurrentEditingVersion['currentEnvironmentId'] = developmentEnv.id;
- }
-
- let shouldFreezeEditor = false;
- if (appCurrentEditingVersion) {
- const hasMultiEnvLicense = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT);
- if (hasMultiEnvLicense) {
- const currentEnvironment = await this.appEnvironmentUtilService.get(
- user.organizationId,
- appCurrentEditingVersion['currentEnvironmentId']
- );
- shouldFreezeEditor = currentEnvironment.priority > 1;
+ const prepareResponse = async (app: App, versionId: string) => {
+ let appVersion,
+ updatedVersionId = versionId;
+ if (updatedVersionId) {
+ appVersion = await this.versionRepository.findVersion(updatedVersionId);
} else {
+ appVersion = await this.versionRepository.findVersionsFromApp(app);
+ appVersion = appVersion[0];
+ updatedVersionId = appVersion.id;
+ }
+
+ const pagesForVersion = await this.pageService.findPagesForVersion(updatedVersionId);
+ const eventsForVersion = await this.eventsService.findEventsForVersion(updatedVersionId);
+
+ const appCurrentEditingVersion = JSON.parse(JSON.stringify(appVersion));
+
+ if (
+ appCurrentEditingVersion &&
+ !(await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT))
+ ) {
const developmentEnv = await this.appEnvironmentUtilService.getByPriority(user.organizationId);
appCurrentEditingVersion['currentEnvironmentId'] = developmentEnv.id;
}
- }
- delete appCurrentEditingVersion['app'];
+ let shouldFreezeEditor = false;
+ if (appCurrentEditingVersion) {
+ const hasMultiEnvLicense = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT);
+ if (hasMultiEnvLicense) {
+ const currentEnvironment = await this.appEnvironmentUtilService.get(
+ user.organizationId,
+ appCurrentEditingVersion['currentEnvironmentId']
+ );
+ shouldFreezeEditor = currentEnvironment.priority > 1;
+ } else {
+ const developmentEnv = await this.appEnvironmentUtilService.getByPriority(user.organizationId);
+ appCurrentEditingVersion['currentEnvironmentId'] = developmentEnv.id;
+ }
+ }
- const appData = {
- ...app,
+ delete appCurrentEditingVersion['app'];
+
+ const appData = {
+ ...app,
+ };
+
+ delete appData['editingVersion'];
+
+ const editingVersion = camelizeKeys(appCurrentEditingVersion);
+
+ // Inject app theme
+ const appTheme = await this.organizationThemesUtilService.getTheme(
+ user.organizationId,
+ editingVersion?.globalSettings?.theme?.id
+ );
+
+ editingVersion['globalSettings']['theme'] = appTheme;
+
+ return {
+ ...appData,
+ editing_version: editingVersion,
+ pages: this.appUtilService.mergeDefaultComponentData(pagesForVersion),
+ events: eventsForVersion,
+ should_freeze_editor: app.creationMode === 'GIT' || shouldFreezeEditor,
+ };
};
- delete appData['editingVersion'];
+ const response = await prepareResponse(app, app.appVersions?.[0]?.id);
+ const modules = await this.appUtilService.fetchModules(app, false, undefined);
- const editingVersion = camelizeKeys(appCurrentEditingVersion);
+ response['modules'] = await Promise.all(modules.map((module) => prepareResponse(module, undefined)));
- // Inject app theme
- const appTheme = await this.organizationThemesUtilService.getTheme(
- user.organizationId,
- editingVersion?.globalSettings?.theme?.id
- );
-
- editingVersion['globalSettings']['theme'] = appTheme;
-
- return {
- ...appData,
- editing_version: editingVersion,
- pages: this.appUtilService.mergeDefaultComponentData(pagesForVersion),
- events: eventsForVersion,
- should_freeze_editor: app.creationMode === 'GIT' || shouldFreezeEditor,
- };
+ return response;
}
async update(app: App, user: User, appVersionUpdateDto: AppVersionUpdateDto) {
diff --git a/server/src/modules/workflows/module.ts b/server/src/modules/workflows/module.ts
index 77dfbd0af3..389a754701 100644
--- a/server/src/modules/workflows/module.ts
+++ b/server/src/modules/workflows/module.ts
@@ -71,7 +71,6 @@ export class WorkflowsModule {
WorkflowExecutionNode,
WorkflowExecutionNode,
WorkflowExecutionEdge,
- RolesRepository,
]),
ThrottlerModule.forRootAsync({
imports: [ConfigModule],