diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js
index 519f43f931..4fb0a82eeb 100644
--- a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js
+++ b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js
@@ -326,7 +326,7 @@ const updateComponentLayout = (components, parentId, isCut = false) => {
};
const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentId = undefined) => {
- const parentId = componentParentId ?? component.component?.parent?.split('-').slice(0, -1).join('-');
+ const parentId = componentParentId ?? component.component?.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1];
const parentComponent = allComponents.find((comp) => comp.componentId === parentId);
if (parentComponent) {
diff --git a/frontend/src/Editor/CodeBuilder/Elements/Color.jsx b/frontend/src/Editor/CodeBuilder/Elements/Color.jsx
index bdeb8f8ac1..a6323be340 100644
--- a/frontend/src/Editor/CodeBuilder/Elements/Color.jsx
+++ b/frontend/src/Editor/CodeBuilder/Elements/Color.jsx
@@ -4,6 +4,8 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import classNames from 'classnames';
import { computeColor } from '@/_helpers/utils';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+import { Tooltip } from 'react-bootstrap';
export const Color = ({
value,
@@ -12,11 +14,12 @@ export const Color = ({
cyLabel,
asBoxShadowPopover = true,
meta,
+ outerWidth = '142px',
component,
styleDefinition,
+ onReset,
}) => {
value = component == 'Button' ? computeColor(styleDefinition, value, meta) : value;
-
const [showPicker, setShowPicker] = useState(false);
const darkMode = localStorage.getItem('darkMode') === 'true';
const colorPickerPosition = meta?.colorPickerPosition ?? '';
@@ -28,7 +31,7 @@ export const Color = ({
left: '0px',
};
const outerStyles = {
- width: '142px',
+ width: outerWidth,
height: '32px',
borderRadius: ' 6px',
display: 'flex',
@@ -109,6 +112,15 @@ export const Color = ({
{value}
+ {typeof onReset === 'function' && (
+
+
Reset to default}>
+
+
+
+
+
+ )}
);
};
diff --git a/frontend/src/ToolJetUI/SwitchGroup/ToggleGroupItem.jsx b/frontend/src/ToolJetUI/SwitchGroup/ToggleGroupItem.jsx
index 439f58af19..ea054f421c 100644
--- a/frontend/src/ToolJetUI/SwitchGroup/ToggleGroupItem.jsx
+++ b/frontend/src/ToolJetUI/SwitchGroup/ToggleGroupItem.jsx
@@ -3,9 +3,10 @@ import React from 'react';
import * as ToggleGroup from '@radix-ui/react-toggle-group';
import SolidIcon from '@/_ui/Icon/SolidIcons';
-const ToggleGroupItem = ({ children, value, isIcon, ...restProps }) => {
+const ToggleGroupItem = ({ children, value, isIcon, className, ...restProps }) => {
return (
-
+
+ {' '}
{!isIcon ? (
children
diff --git a/frontend/src/_components/OverflowTooltip.jsx b/frontend/src/_components/OverflowTooltip.jsx
index f20669e746..297f17d236 100644
--- a/frontend/src/_components/OverflowTooltip.jsx
+++ b/frontend/src/_components/OverflowTooltip.jsx
@@ -24,6 +24,7 @@ export default function OverflowTooltip({ children, className, whiteSpace = 'now
>
{
- if (ref.current && !ref.current.contains(event.target)) {
+ if (ref?.current && !ref.current.contains(event.target)) {
clearSearchText();
// Your function to be triggered
}
diff --git a/frontend/src/_components/SortableList/SortableList.jsx b/frontend/src/_components/SortableList/SortableList.jsx
index 42273bcbd9..7a59e7fda9 100644
--- a/frontend/src/_components/SortableList/SortableList.jsx
+++ b/frontend/src/_components/SortableList/SortableList.jsx
@@ -4,26 +4,26 @@ import { SortableContext, arrayMove, sortableKeyboardCoordinates } from '@dnd-ki
import { SortableItem } from './components';
import { useAppVersionStore } from '@/_stores/appVersionStore';
import { shallow } from 'zustand/shallow';
+import useStore from '@/AppBuilder/_stores/store';
export function SortableList({ items, onChange, renderItem }) {
const sensors = useSensors(
- useSensor(PointerSensor),
+ useSensor(PointerSensor, {
+ activationConstraint: { delay: 150 },
+ }),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
- const { enableReleasedVersionPopupState, isVersionReleased } = useAppVersionStore(
- (state) => ({
- enableReleasedVersionPopupState: state.actions.enableReleasedVersionPopupState,
- isVersionReleased: state.isVersionReleased,
- }),
- shallow
- );
+
+ const shouldFreeze = useStore((state) => state.isVersionReleased || state.isEditorFreezed);
+ const enableReleasedVersionPopupState = useStore((state) => state.enableReleasedVersionPopupState);
+
return (
{
- if (isVersionReleased) {
+ if (shouldFreeze) {
enableReleasedVersionPopupState();
return;
}
diff --git a/frontend/src/_components/SortableList/components/SortableItem.jsx b/frontend/src/_components/SortableList/components/SortableItem.jsx
index 383ac4ced1..345aa9aee5 100644
--- a/frontend/src/_components/SortableList/components/SortableItem.jsx
+++ b/frontend/src/_components/SortableList/components/SortableItem.jsx
@@ -29,7 +29,7 @@ export function SortableItem({ children, id, classNames }) {
return (
-
+
{children}
@@ -51,3 +51,16 @@ export function DragHandle({ show = true }) {
);
}
+
+// hoc for wrapping components that need to be draggable
+export function withDraggable(Component) {
+ return function DraggableComponent(props) {
+ const { attributes, listeners, ref } = useContext(SortableItemContext);
+
+ return (
+
+
+
+ );
+ };
+}
diff --git a/frontend/src/_components/SortableList/index.js b/frontend/src/_components/SortableList/index.js
index 8146d4c14e..375b7add58 100644
--- a/frontend/src/_components/SortableList/index.js
+++ b/frontend/src/_components/SortableList/index.js
@@ -1,36 +1,41 @@
import React from 'react';
+import useStore from '@/AppBuilder/_stores/store';
+import EmptyIllustration from '@assets/images/no-results.svg';
import { SortableList } from './SortableList';
import { DragHandle } from './components';
+import { shallow } from 'zustand/shallow';
+import _ from 'lodash';
const SortableComponent = ({ data, Element, ...restProps }) => {
- const { onSort } = restProps;
+ const allpages = useStore((state) => _.get(state, 'modules.canvas.pages', []), shallow);
+ const reorderPages = useStore((state) => state.reorderPages);
- const [items, setItems] = React.useState([]);
+ const showSearch = useStore((state) => state.showSearch);
+ const pageSearchResults = useStore((state) => state.pageSearchResults);
- React.useEffect(() => {
- setItems(data);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [JSON.stringify(data)]);
+ const pagesTorender =
+ showSearch && pageSearchResults !== null
+ ? allpages.filter((page) => pageSearchResults.includes(page.id))
+ : allpages;
- //function to check if the item in items array has changed position with respect to the original data
- const didItemChangePosition = (originalArr, sortedArry) => {
- return originalArr.some((item, index) => {
- return item.id !== sortedArry[index].id;
- });
- };
-
- React.useEffect(() => {
- if (items.length > 0 && didItemChangePosition(data, items)) {
- onSort(items);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [items]);
+ if (pagesTorender.length === 0) {
+ return (
+
+ );
+ }
return (
(
diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js
index 943d363979..5875a021c9 100644
--- a/frontend/src/_helpers/appUtils.js
+++ b/frontend/src/_helpers/appUtils.js
@@ -1856,7 +1856,7 @@ const updateComponentLayout = (components, parentId, isCut = false) => {
};
//
const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentId = undefined) => {
- const parentId = componentParentId ?? component.component?.parent?.split('-').slice(0, -1).join('-');
+ const parentId = componentParentId ?? component.component?.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1];
const parentComponent = allComponents.find((comp) => comp.componentId === parentId);
diff --git a/frontend/src/_services/appVersion.service.js b/frontend/src/_services/appVersion.service.js
index 85a462a855..d4ce1edb0f 100644
--- a/frontend/src/_services/appVersion.service.js
+++ b/frontend/src/_services/appVersion.service.js
@@ -93,6 +93,9 @@ function autoSaveApp(
global_settings: {
update: { ...diff },
},
+ page_settings: {
+ update: { ...diff },
+ },
};
const body = !type
diff --git a/frontend/src/_styles/components.scss b/frontend/src/_styles/components.scss
index df518ec0b2..9399081d0f 100644
--- a/frontend/src/_styles/components.scss
+++ b/frontend/src/_styles/components.scss
@@ -125,7 +125,11 @@ $btn-dark-color: #FFFFFF;
.page-selector-panel-body {
height: 100%;
padding: 12px 16px;
- background-color: var(--base);
+ border-right: 1px solid #DFE3E6;
+
+ &.dark-theme {
+ border-right: 1px solid var(--slate7);
+ }
.page-handler {
height: 32px !important;
diff --git a/frontend/src/_styles/left-sidebar.scss b/frontend/src/_styles/left-sidebar.scss
index 50866f9595..0817aa8727 100644
--- a/frontend/src/_styles/left-sidebar.scss
+++ b/frontend/src/_styles/left-sidebar.scss
@@ -214,6 +214,23 @@
}
}
+.expired-gradient-border {
+ border: none !important;
+ position: relative;
+ background-color: var(--slate3) !important;
+ color: var(--indigo9) !important;
+}
+
+.expired-gradient-border::before {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: var(--upgrade-default);
+}
+
.debugger-content {
background-color: var(--base);
@@ -366,7 +383,7 @@
}
.modal-searchbar {
- width: 200px;
+ width: 200px !important;
height: 36px;
float: right;
margin-right: 3.5rem !important;
@@ -440,6 +457,17 @@
}
}
+.select-datasource-list-modal {
+ .form-control:focus {
+ padding: 0px;
+ width: 200px !important;
+ }
+
+ .modal-body-content {
+ scrollbar-width: none;
+ }
+}
+
.modal-body {
.datasource-modal-sidebar-footer {
border: 1px solid var(--slate5);
@@ -543,6 +571,32 @@
.page-handler-wrapper {
background: transparent;
+
+ .tj-list-item-selected {
+ .custom-icon {
+ svg {
+ color: #4368E3;
+ stroke: #4368E3;
+
+ }
+ }
+ }
+ .custom-icon {
+ svg {
+ color: #6A727C;
+ stroke: #6A727C;
+ }
+ }
+
+ &.dark-theme {
+ .custom-icon {
+ svg {
+ color: #ffffff;
+ stroke: #ffffff;
+ }
+ }
+ }
+// }
}
@@ -657,10 +711,10 @@
align-items: center;
flex-direction: column;
position: absolute;
- bottom: 0;
+ bottom: 0px;
padding-bottom: 8px;
width: 44px;
- max-height: 180px;
+ max-height: 230px;
}
.tj-leftsidebar-icon-items {
diff --git a/frontend/src/_styles/pages-sidebar.scss b/frontend/src/_styles/pages-sidebar.scss
index 8f4036549b..9c9e0616ed 100644
--- a/frontend/src/_styles/pages-sidebar.scss
+++ b/frontend/src/_styles/pages-sidebar.scss
@@ -3,26 +3,41 @@
}
.viewer {
- .page-name, .navigation-area, .tj-list-item, .canvas-box {
+
+ .page-name,
+ .navigation-area,
+ .tj-list-item,
+ .canvas-box {
transition: var(--tran-01);
}
+ .page-name {
+ font-size: 14px;
+ }
+
.navigation-area {
z-index: 2;
border-right: 1px solid var(--slate6);
+ .left-sidebar-header-btn {
+ width: 25px;
+ height: 25px;
+ }
+
.tj-list-item {
width: 96%;
+ justify-content: start;
}
&.close {
- transform: translateX(-90%);
+ transform: translateX(-90%);
.tj-list-item {
opacity: 0;
display: none;
}
}
+
.pin {
position: absolute;
right: -30px;
@@ -30,13 +45,173 @@
}
&.sidebar-overlay:hover {
- transform: translateX(0%);
- box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
+ transform: translateX(0%);
+ box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
- .tj-list-item {
- opacity: 1;
- display: unset;
+ .tj-list-item {
+ opacity: 1;
+ display: flex;
+ }
+ }
+
+ &.icon-only {
+ width: 65px !important;
+ padding: 0.5rem;
+
+ .tj-list-item {
+ justify-content: center;
+
+ .custom-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
+ }
+ }
+ .accordion-item{
+ border: none;
+ }
+ .accordion-body{
+ padding: 4px 0 4px 16px !important;
+ }
+ .accordion-header{
+ height: auto !important;
+ position: relative;
+ .page-group{
+ height: 32px;
+ display: flex;
+ align-items: center;
+ svg{
+ height: 18px;
+ width: 18px;
+ }
+ }
+ .tj-list-item{
+ border:none !important;
+ padding-left: 8px !important;
+ outline: none !important;
+ &:hover{
+ background: none !important;
+ }
+ }
+ .active-page-group-highlight{
+ height: 32px;
+ width: 5px;
+ background: var(--primary-brand);
+ position: absolute;
+ left: -1rem;
+ }
+ }
+ .accordion-header button{
+ margin: 0;
+ padding: 0 !important;
}
}
+
+}
+
+.pages-settings {
+ .label-style {
+ width: unset !important;
+
+ .ToggleGroupItem {
+ width: unset !important;
+
+ .toggle-item {
+ width: unset !important;
+ padding: 6px;
+ ;
+ }
+ }
+
+ }
+
+ .settings-tab {
+ .field {
+ label {
+ width: 100px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+ }
+}
+
+.icon-selector {
+ width: 25px;
+ height: 25px;
+ margin-right: 6px;
+ justify-content: center;
+
+ .selector-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ &:hover {
+ background: var(--slate7);
+ border-radius: 6px;
+ }
+}
+
+.page-menu-item {
+ .icon-selector {
+ svg {
+ color: #6A727C;
+ stroke: #6A727C;
+
+ }
+ }
+ &.is-selected {
+ .icon-selector {
+ svg {
+ color: #4368E3;
+ stroke: #4368E3;
+
+ }
+ }
+ }
+ &.dark-theme {
+ .icon-selector {
+ svg {
+ color: #ffffff;
+ stroke: #ffffff;
+ }
+ }
+ }
+}
+
+.active-group{
+ border-left: 2px solid red;
+}
+
+.page-group-collapse-icon{
+ margin-right: 6px;
+ position: relative;
+ top: 1px;
+}
+
+.page-drag-overlay {
+ background: var(--slate5);
+ svg {
+ margin-right: 12px;
+ width: 20px !important;
+ height: 20px !important;
+ color:#6A727C;
+ }
+ &.dark-theme{
+ color: #fff;
+ svg{
+ color:#fff;
+ }
+ }
+}
+
+.rename-input-buttons{
+ button{
+ box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.10);
+ border: 1px solid #CCD1D5;
+ }
}
\ No newline at end of file
diff --git a/frontend/src/_styles/popover.scss b/frontend/src/_styles/popover.scss
index 2dad65cc30..45b24463cb 100644
--- a/frontend/src/_styles/popover.scss
+++ b/frontend/src/_styles/popover.scss
@@ -20,7 +20,6 @@
will-change: initial;
}
-
.PopoverArrow {
fill: white;
}
diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss
index f81e2897f5..5a9919a2b4 100644
--- a/frontend/src/_styles/theme.scss
+++ b/frontend/src/_styles/theme.scss
@@ -3016,6 +3016,19 @@ input:focus-visible {
cursor: pointer;
}
+.color-reset {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 20px;
+ width: 25px;
+ margin-right: 5px;
+ border-radius: 5px;
+ &:hover{
+ background: var(--slate1);
+ }
+}
+
.app-sharing-modal {
.form-control.is-invalid,
diff --git a/frontend/src/_ui/FolderList/FolderList.jsx b/frontend/src/_ui/FolderList/FolderList.jsx
index c97495ab0e..2be2112896 100644
--- a/frontend/src/_ui/FolderList/FolderList.jsx
+++ b/frontend/src/_ui/FolderList/FolderList.jsx
@@ -5,6 +5,7 @@ import Skeleton from 'react-loading-skeleton';
import { ButtonSolid } from '../AppButton/AppButton';
import Overlay from 'react-bootstrap/Overlay';
import cx from 'classnames';
+import { Tooltip } from 'react-tooltip'; // Import Tooltip
function FolderList({
overlayFunctionParam,
@@ -23,6 +24,10 @@ function FolderList({
overLayComponent,
darkMode,
toolTipText,
+ disableHoverOption = false,
+ customStyles,
+ CustomIcon,
+ toolTipDisabled = false,
...restProps
}) {
const [isHovered, setIsHovered] = useState(false);
@@ -49,6 +54,8 @@ function FolderList({
setIsHoveredInside(false);
};
+ const computedStyles = customStyles ? customStyles(selectedItem, isHovered) : {};
+
return (
<>
{!isLoading ? (
@@ -59,13 +66,17 @@ function FolderList({
'tj-list-item-disabled': disabled,
'tj-list-item-option-opened': showGroupOptions,
})}
- style={backgroundColor && { backgroundColor }}
+ style={{
+ ...(backgroundColor && { backgroundColor }),
+ ...{ ...computedStyles.pill, ...computedStyles.text },
+ }}
onClick={isHoveredInside ? menuToggle : onClick}
data-cy={`${dataCy}-list-item`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
data-tooltip-content={toolTipText}
data-tooltip-id="button-content"
+ data-tooltip-hidden={!toolTipDisabled}
>
{LeftIcon && (
@@ -73,10 +84,24 @@ function FolderList({
)}
+ {CustomIcon && (
+
+
+
+ )}
+
{children}
{RightIcon && {RightIcon && }
}
- {overLayComponent && (isHovered || showGroupOptions) && (
+ {overLayComponent && ((!disableHoverOption && isHovered) || showGroupOptions) && (
<>
)}
+
+
>
);
}
diff --git a/frontend/src/_ui/Icon/solidIcons/Reset.jsx b/frontend/src/_ui/Icon/solidIcons/Reset.jsx
new file mode 100644
index 0000000000..74dceefd11
--- /dev/null
+++ b/frontend/src/_ui/Icon/solidIcons/Reset.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+
+const Reset = ({ fill = '#6A727C', width = '10', className = '', viewBox = '0 0 10 10' }) => (
+
+
+
+);
+
+export default Reset;
diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js
index 7e6874ca09..02ce6bc09a 100644
--- a/frontend/src/_ui/Icon/solidIcons/index.js
+++ b/frontend/src/_ui/Icon/solidIcons/index.js
@@ -173,6 +173,7 @@ import Search01 from './Search01.jsx';
import ShiftButtonIcon from './ShiftButtonIcon.jsx';
import Unpin01 from './Unpin01.jsx';
import WarningUserNotFound from './WarningUserNotFound.jsx';
+import Reset from './Reset.jsx';
const Icon = (props) => {
switch (props.name) {
@@ -394,6 +395,8 @@ const Icon = (props) => {
return ;
case 'row':
return
;
+ case 'reset':
+ return ;
case 'sadrectangle':
return ;
case 'search':
diff --git a/server/data-migrations/1718357264489-MoveHiddenFieldInAppVersionsToPageSettings.ts b/server/data-migrations/1718357264489-MoveHiddenFieldInAppVersionsToPageSettings.ts
new file mode 100644
index 0000000000..1e20c9ba71
--- /dev/null
+++ b/server/data-migrations/1718357264489-MoveHiddenFieldInAppVersionsToPageSettings.ts
@@ -0,0 +1,38 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class MoveHiddenFieldInAppVersionsToPageSettings1718357264489 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.startTransaction();
+ try {
+ const pagesWitHiddenTrue = await queryRunner.query(
+ `SELECT id, show_viewer_navigation FROM app_versions WHERE show_viewer_navigation = 'true'`
+ );
+ const pagesWithHiddenFalse = await queryRunner.query(
+ `SELECT id, show_viewer_navigation FROM app_versions WHERE show_viewer_navigation = 'false'`
+ );
+ const idsToUpdate = pagesWitHiddenTrue.map((page) => page.id);
+ const idsToUpdateFalse = pagesWithHiddenFalse.map((page) => page.id);
+
+ if (idsToUpdate.length > 0) {
+ const quotedIds = idsToUpdate.map((id) => `'${id}'`).join(',');
+ await queryRunner.query(
+ `UPDATE app_versions SET page_settings = '{"properties": {"disableMenu": {"value": "{{false}}", "fxActive": false}}}' WHERE id IN (${quotedIds})`
+ );
+ }
+ if (idsToUpdateFalse.length > 0) {
+ const quotedIds = idsToUpdateFalse.map((id) => `'${id}'`).join(',');
+ await queryRunner.query(
+ `UPDATE app_versions SET page_settings = '{"properties": {"disableMenu": {"value": "{{true}}", "fxActive": false}}}' WHERE id IN (${quotedIds})`
+ );
+ }
+
+ await queryRunner.commitTransaction();
+ } catch (error) {
+ await queryRunner.rollbackTransaction();
+ throw error;
+ }
+ }
+ public async down(queryRunner: QueryRunner): Promise {
+ return Promise.resolve();
+ }
+}
diff --git a/server/migrations/1716890766240-AddPageSettingsColumnToAppVersionTable.ts b/server/migrations/1716890766240-AddPageSettingsColumnToAppVersionTable.ts
new file mode 100644
index 0000000000..478cb5ecdb
--- /dev/null
+++ b/server/migrations/1716890766240-AddPageSettingsColumnToAppVersionTable.ts
@@ -0,0 +1,18 @@
+import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
+
+export class AddPageSettingsColumnToAppVersionTable1716890766240 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.addColumn(
+ 'app_versions',
+ new TableColumn({
+ name: 'page_settings',
+ type: 'json',
+ isNullable: true,
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.dropColumn('app_versions', 'page_settings');
+ }
+}
diff --git a/server/migrations/1716921638529-AddIconFieldToPagesTable.ts b/server/migrations/1716921638529-AddIconFieldToPagesTable.ts
new file mode 100644
index 0000000000..045f37d45e
--- /dev/null
+++ b/server/migrations/1716921638529-AddIconFieldToPagesTable.ts
@@ -0,0 +1,18 @@
+import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
+
+export class AddIconFieldToPagesTable1716921638529 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.addColumn(
+ 'pages',
+ new TableColumn({
+ name: 'icon',
+ type: 'varchar',
+ isNullable: true,
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.dropColumn('pages', 'icon');
+ }
+}
diff --git a/server/src/controllers/apps.controller.v2.ts b/server/src/controllers/apps.controller.v2.ts
index a3fa3a39c9..00ef864b35 100644
--- a/server/src/controllers/apps.controller.v2.ts
+++ b/server/src/controllers/apps.controller.v2.ts
@@ -230,7 +230,7 @@ export class AppsControllerV2 {
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
- @Put(':id/versions/:versionId/global_settings')
+ @Put([':id/versions/:versionId/global_settings', ':id/versions/:versionId/page_settings'])
async updateGlobalSettings(
@User() user,
@Param('id') id,
@@ -408,6 +408,23 @@ export class AppsControllerV2 {
await this.pageService.updatePage(updatePageDto, versionId);
}
+ @UseGuards(JwtAuthGuard)
+ @UseInterceptors(ValidAppInterceptor)
+ @Put(':id/versions/:versionId/pages/reorder')
+ async reorderPages(@User() user, @Param('id') id, @Param('versionId') versionId, @Body() reorderPagesDto) {
+ const version = await this.appsService.findVersion(versionId);
+ const app = version.app;
+ if (app.id !== id) {
+ throw new BadRequestException();
+ }
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
+ if (!ability.can(APP_RESOURCE_ACTIONS.VERSION_UPDATE, app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+ console.log(reorderPagesDto, 'payload');
+ await this.pageService.reorderPages(reorderPagesDto, versionId);
+ }
+
@UseGuards(JwtAuthGuard)
@UseInterceptors(ValidAppInterceptor)
@Delete(':id/versions/:versionId/pages')
diff --git a/server/src/dto/app-version-update.dto.ts b/server/src/dto/app-version-update.dto.ts
index 53d79b387a..6c7aadb71c 100644
--- a/server/src/dto/app-version-update.dto.ts
+++ b/server/src/dto/app-version-update.dto.ts
@@ -23,4 +23,7 @@ export class AppVersionUpdateDto {
@IsOptional()
globalSettings: any;
+
+ @IsOptional()
+ pageSettings: any;
}
diff --git a/server/src/dto/pages.dto.ts b/server/src/dto/pages.dto.ts
index 9c84f16ceb..b5d96821fe 100644
--- a/server/src/dto/pages.dto.ts
+++ b/server/src/dto/pages.dto.ts
@@ -23,7 +23,7 @@ export class CreatePageDto {
disabled: boolean;
@IsOptional()
- hidden: boolean;
+ hidden: Record;
@IsOptional()
autoComputeLayout: boolean;
diff --git a/server/src/entities/app_version.entity.ts b/server/src/entities/app_version.entity.ts
index 683d7f7c5d..947dd59a0b 100644
--- a/server/src/entities/app_version.entity.ts
+++ b/server/src/entities/app_version.entity.ts
@@ -31,6 +31,9 @@ export class AppVersion extends BaseEntity {
@Column('simple-json', { name: 'global_settings' })
globalSettings;
+ @Column('simple-json', { name: 'page_settings' })
+ pageSettings;
+
@Column({ name: 'show_viewer_navigation' })
showViewerNavigation: boolean;
diff --git a/server/src/entities/page.entity.ts b/server/src/entities/page.entity.ts
index 52aedf296d..733c269dc9 100644
--- a/server/src/entities/page.entity.ts
+++ b/server/src/entities/page.entity.ts
@@ -28,8 +28,11 @@ export class Page {
@Column()
disabled: boolean;
+ @Column('simple-json', { name: 'hidden' })
+ hidden;
+
@Column()
- hidden: boolean;
+ icon: string;
@CreateDateColumn({ default: () => 'now()', name: 'created_at' })
createdAt: Date;
diff --git a/server/src/helpers/import_export.helpers.ts b/server/src/helpers/import_export.helpers.ts
index 9269743e23..2c98071159 100644
--- a/server/src/helpers/import_export.helpers.ts
+++ b/server/src/helpers/import_export.helpers.ts
@@ -6,14 +6,32 @@ export function updateEntityReferences(node, resourceMapping: Record {
+ // remove curly braces and extract the entity "component.entityName.something"
+ const ref = match.slice(2, -2).trim();
+ const entityName = ref.split('.')[1];
- const entityName = ref.split('.')[1];
+ if (resourceMapping[entityName]) {
+ const newValue = value.replace(entityName, resourceMapping[entityName]);
- if (resourceMapping[entityName]) {
- const newValue = value.replace(entityName, resourceMapping[entityName]);
+ node[key] = newValue;
+ value = newValue;
+ }
+ });
+ } else {
+ // kept this logic for fallback, although it should not be needed
+ const ref = value.replace('{{', '').replace('}}', '');
- node[key] = newValue;
+ const entityName = ref.split('.')[1];
+
+ if (resourceMapping[entityName]) {
+ const newValue = value.replace(entityName, resourceMapping[entityName]);
+
+ node[key] = newValue;
+ }
}
}
} else if (typeof value === 'object') {
@@ -45,11 +63,23 @@ export function findAllEntityReferences(node, allRefs): [] {
const referenceExists = value;
if (referenceExists) {
- const ref = value.replace('{{', '').replace('}}', '');
+ const matches = value.match(/{{(.*?)}}/g);
+ if (matches) {
+ matches.forEach((match) => {
+ const ref = match.slice(2, -2).trim(); // Remove {{ and }}
+ const entityName = ref.split('.')[1];
+ if (entityName && !allRefs.includes(entityName)) {
+ allRefs.push(entityName);
+ }
+ });
+ } else {
+ // kept this logic for fallback, although it should not be needed
+ const ref = value.replace('{{', '').replace('}}', '');
- const entityName = ref.split('.')[1];
+ const entityName = ref.split('.')[1];
- allRefs.push(entityName);
+ allRefs.push(entityName);
+ }
}
} else if (typeof value === 'object') {
findAllEntityReferences(value, allRefs);
diff --git a/server/src/helpers/utils.helper.ts b/server/src/helpers/utils.helper.ts
index fa45d618b8..c23e109d37 100644
--- a/server/src/helpers/utils.helper.ts
+++ b/server/src/helpers/utils.helper.ts
@@ -319,3 +319,35 @@ export const isValidDomain = (email: string, restrictedDomain: string): boolean
export const isHttpsEnabled = () => {
return !!process.env.TOOLJET_HOST?.startsWith('https');
};
+
+export function isObject(obj) {
+ return obj && typeof obj === 'object';
+}
+
+export function mergeDeep(target, source, seen = new WeakMap()) {
+ if (!isObject(target)) {
+ target = {};
+ }
+
+ if (!isObject(source)) {
+ return target;
+ }
+
+ if (seen.has(source)) {
+ return seen.get(source);
+ }
+ seen.set(source, target);
+
+ for (const key in source) {
+ if (isObject(source[key])) {
+ if (!target[key]) {
+ Object.assign(target, { [key]: {} });
+ }
+ mergeDeep(target[key], source[key], seen);
+ } else {
+ Object.assign(target, { [key]: source[key] });
+ }
+ }
+
+ return target;
+}
diff --git a/server/src/services/app_import_export.service.ts b/server/src/services/app_import_export.service.ts
index 699cd12a99..a693db254b 100644
--- a/server/src/services/app_import_export.service.ts
+++ b/server/src/services/app_import_export.service.ts
@@ -794,13 +794,13 @@ export class AppImportExportService {
const isParentTabOrCalendar = isChildOfTabsOrCalendar(component, pageComponents, parentId, true);
if (isParentTabOrCalendar) {
- const childTabId = component.parent.split('-')[component.parent.split('-').length - 1];
- const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const childTabId = component?.parent ? component.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[2] : null;
+ const _parentId = component?.parent ? component.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1] : null;
const mappedParentId = newComponentIdsMap[_parentId];
parentId = `${mappedParentId}-${childTabId}`;
} else if (isChildOfKanbanModal(component, pageComponents, parentId, true)) {
- const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const _parentId = component?.parent ? component.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1] : null;
const mappedParentId = newComponentIdsMap[_parentId];
parentId = `${mappedParentId}-modal`;
@@ -1885,13 +1885,13 @@ function transformComponentData(
);
if (isParentTabOrCalendar) {
- const childTabId = component.parent.split('-')[component.parent.split('-').length - 1];
- const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const childTabId = component?.parent ? component.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[2] : null;
+ const _parentId = component?.parent ? component.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1] : null;
const mappedParentId = componentsMapping[_parentId];
parentId = `${mappedParentId}-${childTabId}`;
} else if (isChildOfKanbanModal(component, allComponents, parentId, true)) {
- const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const _parentId = component?.parent ? component.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1] : null;
const mappedParentId = componentsMapping[_parentId];
parentId = `${mappedParentId}-modal`;
@@ -1940,7 +1940,7 @@ const isChildOfTabsOrCalendar = (
isNormalizedAppDefinitionSchema: boolean
) => {
if (componentParentId) {
- const parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const parentId = component?.parent ? component.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1] : null;
const parentComponent = allComponents.find((comp) => comp.id === parentId);
@@ -1964,7 +1964,7 @@ const isChildOfKanbanModal = (
) => {
if (!componentParentId || !componentParentId.includes('modal')) return false;
- const parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const parentId = component?.parent ? component.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1] : null;
const parentComponent = allComponents.find((comp) => comp.id === parentId);
diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts
index b40f964523..60e5529751 100644
--- a/server/src/services/apps.service.ts
+++ b/server/src/services/apps.service.ts
@@ -10,7 +10,7 @@ import { DataQuery } from 'src/entities/data_query.entity';
import { AppImportExportService } from './app_import_export.service';
import { DataSourcesService } from './data_sources.service';
import { Credential } from 'src/entities/credential.entity';
-import { catchDbException, cleanObject, defaultAppEnvironments } from 'src/helpers/utils.helper';
+import { catchDbException, cleanObject, defaultAppEnvironments, mergeDeep } from 'src/helpers/utils.helper';
import { AppUpdateDto } from '@dto/app-update.dto';
import { viewableAppsQueryUsingPermissions } from 'src/helpers/queries';
import { VersionEditDto } from '@dto/version-edit.dto';
@@ -374,6 +374,7 @@ export class AppsService {
if (versionFrom) {
(appVersion.showViewerNavigation = versionFrom.showViewerNavigation),
(appVersion.globalSettings = versionFrom.globalSettings),
+ (appVersion.pageSettings = versionFrom.pageSettings),
await manager.save(appVersion);
const oldDataQueryToNewMapping = await this.createNewDataSourcesAndQueriesForVersion(
@@ -529,7 +530,7 @@ export class AppsService {
const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentId = undefined) => {
if (componentParentId) {
- const parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const parentId = component?.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1];
const parentComponent = allComponents.find((comp) => comp.id === parentId);
@@ -545,7 +546,7 @@ export class AppsService {
if (!componentParentId.includes('modal')) return false;
if (componentParentId) {
- const parentId = componentParentId.split('-').slice(0, -1).join('-');
+ const parentId = componentParentId.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1];
const isParentKandban = allComponents.find((comp) => comp.id === parentId)?.type === 'Kanban';
return isParentKandban;
@@ -597,8 +598,8 @@ export class AppsService {
const isParentTabOrCalendar = isChildOfTabsOrCalendar(component, page.components, parentId);
if (isParentTabOrCalendar) {
- const childTabId = component.parent.split('-')[component.parent.split('-').length - 1];
- const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const childTabId = component?.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[2];
+ const _parentId = component?.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1];
const mappedParentId = oldComponentToNewComponentMapping[_parentId];
parentId = `${mappedParentId}-${childTabId}`;
@@ -660,12 +661,12 @@ export class AppsService {
if (isParentTabOrCalendar) {
const childTabId = component?.parent?.split('-')[component?.parent?.split('-').length - 1];
- const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const _parentId = component?.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1];
const mappedParentId = oldComponentToNewComponentMapping[_parentId];
parentId = `${mappedParentId}-${childTabId}`;
} else if (isChildOfKanbanModal(component.parent, page.components)) {
- const _parentId = component?.parent?.split('-').slice(0, -1).join('-');
+ const _parentId = component?.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[1];
const mappedParentId = oldComponentToNewComponentMapping[_parentId];
parentId = `${mappedParentId}-modal`;
@@ -1022,7 +1023,7 @@ export class AppsService {
async updateAppVersion(version: AppVersion, body: AppVersionUpdateDto) {
const editableParams = {};
- const { globalSettings, homePageId } = await this.appVersionsRepository.findOne({
+ const { globalSettings, homePageId, pageSettings } = await this.appVersionsRepository.findOne({
where: { id: version.id },
});
@@ -1037,6 +1038,12 @@ export class AppsService {
};
}
+ if (body?.pageSettings) {
+ editableParams['pageSettings'] = {
+ ...mergeDeep(pageSettings, body.pageSettings),
+ };
+ }
+
if (typeof body?.showViewerNavigation === 'boolean') {
editableParams['showViewerNavigation'] = body.showViewerNavigation;
}