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 ( +
+
+ +

+ No pages found +

+
+
+ ); + } 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; }