From ee79e3a2cb9fa530cb5ea4a0ba0a8a37e11162b5 Mon Sep 17 00:00:00 2001 From: Kartik Gupta Date: Tue, 22 Oct 2024 15:49:55 +0530 Subject: [PATCH 1/9] page menu and import export changes --- .../AppBuilder/AppCanvas/appCanvasUtils.js | 2 +- .../LeftSidebar/PageMenu/PageMenu.jsx | 123 +++--------------- frontend/src/_components/SearchBox.jsx | 2 +- .../_components/SortableList/SortableList.jsx | 18 +-- .../SortableList/components/SortableItem.jsx | 15 ++- .../src/_components/SortableList/index.js | 47 ++++--- frontend/src/_helpers/appUtils.js | 2 +- server/src/controllers/apps.controller.v2.ts | 17 +++ server/src/helpers/import_export.helpers.ts | 46 +++++-- .../src/services/app_import_export.service.ts | 16 +-- server/src/services/apps.service.ts | 12 +- 11 files changed, 139 insertions(+), 161 deletions(-) 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/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx index e18f8c5b40..3396e793ba 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx @@ -29,11 +29,9 @@ export const PageMenu = ({ setPinned, }) => { // const pages = useStore((state) => state.pages); - const allpages = useStore((state) => _.get(state, 'modules.canvas.pages', []), shallow); const showAddNewPageInput = useStore((state) => state.showAddNewPageInput); const toggleShowAddNewPageInput = useStore((state) => state.toggleShowAddNewPageInput); const showSearch = useStore((state) => state.showSearch); - const toggleSearch = useStore((state) => state.toggleSearch); const handleSearch = useStore((state) => state.handleSearch); const togglePageSettingMenu = useStore((state) => state.togglePageSettingMenu); const shouldFreeze = useStore((state) => state.getShouldFreeze()); @@ -44,58 +42,6 @@ export const PageMenu = ({ closePageEditPopover(); }; }, []); - // const [allpages, setAllPages] = useState(pages); - // const [haveUserPinned, setHaveUserPinned] = useState(false); - // const currentState = useCurrentState(); - // const [newPageBeingCreated, setNewPageBeingCreated] = useState(false); - // const [showSearch, setShowSearch] = useState(false); - // const { enableReleasedVersionPopupState, isVersionReleased, isEditorFreezed } = useAppVersionStore( - // (state) => ({ - // enableReleasedVersionPopupState: state.actions.enableReleasedVersionPopupState, - // isVersionReleased: state.isVersionReleased, - // isEditorFreezed: state.isEditorFreezed, - // }), - // shallow - // ); - - // useEffect(() => { - // setAllPages(pages); - // }, [pages]); - - // const sortedAllPages = useMemo( - // () => [...allpages.filter((c) => !c.disabled), ...allpages.filter((c) => c.disabled)], - // [allpages] - // ); - - // const filterPages = (value) => { - // if (!value || value.length === 0) return clearSearch(); - - // const fuse = new Fuse(pages, { keys: ['name'], threshold: 0.3 }); - // const result = fuse.search(value); - // setAllPages(result.map((item) => item.item)); - // }; - - // const clearSearch = () => { - // setAllPages(pages); - // }; - - // React.useEffect(() => { - // if (!_.isEqual(pages, allpages)) { - // setAllPages(pages); - // } - - // // eslint-disable-next-line react-hooks/exhaustive-deps - // }, [JSON.stringify({ pages })]); - - // const pinPagesPopover = (state) => { - // if (!haveUserPinned) { - // setPinned(state); - // } - // }; - const license = useStore((state) => state.license); - const isLicensed = - !_.get(license, 'featureAccess.licenseStatus.isExpired', true) && - _.get(license, 'featureAccess.licenseStatus.isLicenseValid', false); const { definition: { styles }, @@ -110,42 +56,19 @@ export const PageMenu = ({ className="card-body p-0 pb-5" > - - // } - > +
- {isLicensed ? ( - (shouldFreeze ? enableReleasedVersionPopupState() : toggleShowAddNewPageInput(true))} - className="left-sidebar-header-btn" - fill={`var(--slate12)`} - darkMode={darkMode} - leftIcon="plus" - iconWidth="14" - variant="tertiary" - disabled={shouldFreeze} - > - ) : ( - (shouldFreeze ? enableReleasedVersionPopupState() : toggleShowAddNewPageInput(true))} - className="left-sidebar-header-btn" - fill={`var(--slate12)`} - darkMode={darkMode} - leftIcon="plus" - iconWidth="14" - variant="tertiary" - disabled={shouldFreeze} - > - )} + (shouldFreeze ? enableReleasedVersionPopupState() : toggleShowAddNewPageInput(true))} + className="left-sidebar-header-btn" + fill={`var(--slate12)`} + darkMode={darkMode} + leftIcon="plus" + iconWidth="14" + variant="tertiary" + disabled={shouldFreeze} + > - {isLicensed ? ( - // - - ) : ( - - )} + {showAddNewPageInput && (
diff --git a/frontend/src/_components/SearchBox.jsx b/frontend/src/_components/SearchBox.jsx index ec52e37fa1..1e910dd519 100644 --- a/frontend/src/_components/SearchBox.jsx +++ b/frontend/src/_components/SearchBox.jsx @@ -39,7 +39,7 @@ export const SearchBox = forwardRef( }; const handleClickOutside = (event) => { - 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/server/src/controllers/apps.controller.v2.ts b/server/src/controllers/apps.controller.v2.ts index 6ce6650776..c4e5d7f54c 100644 --- a/server/src/controllers/apps.controller.v2.ts +++ b/server/src/controllers/apps.controller.v2.ts @@ -407,6 +407,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/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/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..6270ff9378 100644 --- a/server/src/services/apps.service.ts +++ b/server/src/services/apps.service.ts @@ -529,7 +529,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 +545,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 +597,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 +660,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`; From fbcfe34d939bee63517209fc522a48664b3d4083 Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Tue, 22 Oct 2024 17:08:53 +0530 Subject: [PATCH 2/9] fix page menu UI and settings --- .../LeftSidebar/PageMenu/PageMenu.jsx | 123 ++++++++++-- frontend/src/AppBuilder/Viewer/Viewer.jsx | 4 +- .../src/Editor/CodeBuilder/Elements/Color.jsx | 16 +- .../ToolJetUI/SwitchGroup/ToggleGroupItem.jsx | 5 +- frontend/src/_components/OverflowTooltip.jsx | 1 + frontend/src/_services/appVersion.service.js | 3 + frontend/src/_styles/components.scss | 6 +- frontend/src/_styles/left-sidebar.scss | 60 +++++- frontend/src/_styles/pages-sidebar.scss | 189 +++++++++++++++++- frontend/src/_styles/popover.scss | 1 - frontend/src/_styles/theme.scss | 13 ++ frontend/src/_ui/FolderList/FolderList.jsx | 33 ++- frontend/src/_ui/Icon/solidIcons/Reset.jsx | 21 ++ frontend/src/_ui/Icon/solidIcons/index.js | 3 + ...eHiddenFieldInAppVersionsToPageSettings.ts | 38 ++++ ...-AddPageSettingsColumnToAppVersionTable.ts | 18 ++ .../1716921638529-AddIconFieldToPagesTable.ts | 18 ++ server/src/controllers/apps.controller.v2.ts | 2 +- server/src/dto/app-version-update.dto.ts | 3 + server/src/dto/pages.dto.ts | 2 +- server/src/entities/app_version.entity.ts | 3 + server/src/entities/page.entity.ts | 5 +- server/src/helpers/utils.helper.ts | 32 +++ server/src/services/apps.service.ts | 11 +- 24 files changed, 562 insertions(+), 48 deletions(-) create mode 100644 frontend/src/_ui/Icon/solidIcons/Reset.jsx create mode 100644 server/data-migrations/1718357264489-MoveHiddenFieldInAppVersionsToPageSettings.ts create mode 100644 server/migrations/1716890766240-AddPageSettingsColumnToAppVersionTable.ts create mode 100644 server/migrations/1716921638529-AddIconFieldToPagesTable.ts diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx index 3396e793ba..e18f8c5b40 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx @@ -29,9 +29,11 @@ export const PageMenu = ({ setPinned, }) => { // const pages = useStore((state) => state.pages); + const allpages = useStore((state) => _.get(state, 'modules.canvas.pages', []), shallow); const showAddNewPageInput = useStore((state) => state.showAddNewPageInput); const toggleShowAddNewPageInput = useStore((state) => state.toggleShowAddNewPageInput); const showSearch = useStore((state) => state.showSearch); + const toggleSearch = useStore((state) => state.toggleSearch); const handleSearch = useStore((state) => state.handleSearch); const togglePageSettingMenu = useStore((state) => state.togglePageSettingMenu); const shouldFreeze = useStore((state) => state.getShouldFreeze()); @@ -42,6 +44,58 @@ export const PageMenu = ({ closePageEditPopover(); }; }, []); + // const [allpages, setAllPages] = useState(pages); + // const [haveUserPinned, setHaveUserPinned] = useState(false); + // const currentState = useCurrentState(); + // const [newPageBeingCreated, setNewPageBeingCreated] = useState(false); + // const [showSearch, setShowSearch] = useState(false); + // const { enableReleasedVersionPopupState, isVersionReleased, isEditorFreezed } = useAppVersionStore( + // (state) => ({ + // enableReleasedVersionPopupState: state.actions.enableReleasedVersionPopupState, + // isVersionReleased: state.isVersionReleased, + // isEditorFreezed: state.isEditorFreezed, + // }), + // shallow + // ); + + // useEffect(() => { + // setAllPages(pages); + // }, [pages]); + + // const sortedAllPages = useMemo( + // () => [...allpages.filter((c) => !c.disabled), ...allpages.filter((c) => c.disabled)], + // [allpages] + // ); + + // const filterPages = (value) => { + // if (!value || value.length === 0) return clearSearch(); + + // const fuse = new Fuse(pages, { keys: ['name'], threshold: 0.3 }); + // const result = fuse.search(value); + // setAllPages(result.map((item) => item.item)); + // }; + + // const clearSearch = () => { + // setAllPages(pages); + // }; + + // React.useEffect(() => { + // if (!_.isEqual(pages, allpages)) { + // setAllPages(pages); + // } + + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [JSON.stringify({ pages })]); + + // const pinPagesPopover = (state) => { + // if (!haveUserPinned) { + // setPinned(state); + // } + // }; + const license = useStore((state) => state.license); + const isLicensed = + !_.get(license, 'featureAccess.licenseStatus.isExpired', true) && + _.get(license, 'featureAccess.licenseStatus.isLicenseValid', false); const { definition: { styles }, @@ -56,19 +110,42 @@ export const PageMenu = ({ className="card-body p-0 pb-5" > - + + // } + >
- (shouldFreeze ? enableReleasedVersionPopupState() : toggleShowAddNewPageInput(true))} - className="left-sidebar-header-btn" - fill={`var(--slate12)`} - darkMode={darkMode} - leftIcon="plus" - iconWidth="14" - variant="tertiary" - disabled={shouldFreeze} - > + {isLicensed ? ( + (shouldFreeze ? enableReleasedVersionPopupState() : toggleShowAddNewPageInput(true))} + className="left-sidebar-header-btn" + fill={`var(--slate12)`} + darkMode={darkMode} + leftIcon="plus" + iconWidth="14" + variant="tertiary" + disabled={shouldFreeze} + > + ) : ( + (shouldFreeze ? enableReleasedVersionPopupState() : toggleShowAddNewPageInput(true))} + className="left-sidebar-header-btn" + fill={`var(--slate12)`} + darkMode={darkMode} + leftIcon="plus" + iconWidth="14" + variant="tertiary" + disabled={shouldFreeze} + > + )} - + {isLicensed ? ( + // + + ) : ( + + )} {showAddNewPageInput && (
diff --git a/frontend/src/AppBuilder/Viewer/Viewer.jsx b/frontend/src/AppBuilder/Viewer/Viewer.jsx index 35e026f67a..8bd548fe88 100644 --- a/frontend/src/AppBuilder/Viewer/Viewer.jsx +++ b/frontend/src/AppBuilder/Viewer/Viewer.jsx @@ -97,8 +97,6 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod const switchPage = useStore((state) => state.switchPage); const showHeader = !globalSettings?.hideHeader && isAppLoaded; - const isLicenseValid = useStore((state) => state.isLicenseValid); - const licenseValid = isLicenseValid(); // ---remove const handleAppEnvironmentChanged = useCallback((environment) => { console.log('setAppVersionCurrentEnvironment', environment); @@ -225,7 +223,7 @@ export const Viewer = ({ id: appId, darkMode, moduleId = 'canvas', switchDarkMod )}
- {/* {!licenseValid && isAppLoaded && } */} + {isAppLoaded && } {isMobilePreviewMode &&
} {isMobilePreviewMode &&
}
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 >
{!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 c4e5d7f54c..8f874a1d97 100644 --- a/server/src/controllers/apps.controller.v2.ts +++ b/server/src/controllers/apps.controller.v2.ts @@ -229,7 +229,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, 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/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/apps.service.ts b/server/src/services/apps.service.ts index 6270ff9378..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( @@ -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; } From 37312e1ffd70c89aea43ff3c3a240997b3ae06df Mon Sep 17 00:00:00 2001 From: Shaurya Sharma Date: Tue, 22 Oct 2024 18:45:30 +0530 Subject: [PATCH 3/9] Merge fixes --- .../_stores/slices/queryPanelSlice.js | 18 ---- frontend/src/_components/AccordionForm.jsx | 22 +++++ frontend/src/_components/DynamicForm.jsx | 4 +- frontend/src/_ui/Accordion/AccordionItem.js | 21 ++-- frontend/src/_ui/Sort/QueryEditor.jsx | 74 ++++++++++++++ frontend/src/_ui/Sort/SourceEditor.jsx | 80 ++++++++++++++++ frontend/src/_ui/Sort/index.js | 53 ++++++++++ frontend/src/_ui/Sort/sortStyles.scss | 96 +++++++++++++++++++ 8 files changed, 338 insertions(+), 30 deletions(-) create mode 100644 frontend/src/_components/AccordionForm.jsx create mode 100644 frontend/src/_ui/Sort/QueryEditor.jsx create mode 100644 frontend/src/_ui/Sort/SourceEditor.jsx create mode 100644 frontend/src/_ui/Sort/index.js create mode 100644 frontend/src/_ui/Sort/sortStyles.scss diff --git a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js index 12a538400e..27a59740db 100644 --- a/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js @@ -205,7 +205,6 @@ export const createQueryPanelSlice = (set, get) => ({ setPreviewPanelExpanded, executeRunPycode, runTransformation, - executeWorkflow, executeMultilineJS, } = queryPanel; const { onEvent } = eventsSlice; @@ -297,14 +296,6 @@ export const createQueryPanelSlice = (set, get) => ({ queryExecutionPromise = executeMultilineJS(query.options.code, query?.id, false, mode, parameters); } else if (query.kind === 'runpy') { queryExecutionPromise = executeRunPycode(query.options.code, query, false, mode, queryState); - } else if (query.kind === 'workflows') { - queryExecutionPromise = executeWorkflow( - moduleId, - query.options.workflowId, - query.options.blocking, - query.options?.params, - (currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id //TODO: currentAppEnvironmentId may no longer required. Need to check - ); } else { queryExecutionPromise = dataqueryService.run( queryId, @@ -465,7 +456,6 @@ export const createQueryPanelSlice = (set, get) => ({ setPreviewPanelExpanded, executeRunPycode, runTransformation, - executeWorkflow, executeMultilineJS, setIsPreviewQueryLoading, } = queryPanel; @@ -514,14 +504,6 @@ export const createQueryPanelSlice = (set, get) => ({ queryExecutionPromise = executeMultilineJS(query.options.code, query?.id, true, '', parameters); } else if (query.kind === 'runpy') { queryExecutionPromise = executeRunPycode(query.options.code, query, true, 'edit', queryState); - } else if (query.kind === 'workflows') { - queryExecutionPromise = executeWorkflow( - moduleId, - query.options.workflowId, - query.options.blocking, - query.options?.params, - (currentAppEnvironmentId ?? environmentId) || selectedEnvironment?.id //TODO: currentAppEnvironmentId may no longer required. Need to check - ); } else { queryExecutionPromise = dataqueryService.preview(query, options, currentVersionId, currentAppEnvironmentId); } diff --git a/frontend/src/_components/AccordionForm.jsx b/frontend/src/_components/AccordionForm.jsx new file mode 100644 index 0000000000..6afe396bb0 --- /dev/null +++ b/frontend/src/_components/AccordionForm.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import isEmpty from 'lodash/isEmpty'; +import Accordion from '@/_ui/Accordion'; + +const AccordionForm = ({ formComponent, getLayout }) => { + const sections = Object.keys(formComponent) + .map((key) => ({ + title: formComponent[key].title, + inputs: formComponent[key].inputs, + })) + .filter(({ inputs }) => inputs && !isEmpty(inputs)); + + const items = sections.map(({ title, inputs }) => ({ + title: title, + isOpen: true, + children: getLayout(inputs), + })); + + return ; +}; + +export default AccordionForm; diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx index 37fd3104af..13734d54b9 100644 --- a/frontend/src/_components/DynamicForm.jsx +++ b/frontend/src/_components/DynamicForm.jsx @@ -8,13 +8,13 @@ import OAuth from '@/_ui/OAuth'; import Toggle from '@/_ui/Toggle'; import OpenApi from '@/_ui/OpenAPI'; import { Checkbox, CheckboxGroup } from '@/_ui/CheckBox'; -import CodeHinter from '@/Editor/CodeEditor'; +import CodeHinter from '@/AppBuilder/CodeEditor'; import GoogleSheets from '@/_components/Googlesheets'; import Slack from '@/_components/Slack'; import Zendesk from '@/_components/Zendesk'; import { ConditionFilter, CondtionSort, MultiColumn } from '@/_components/MultiConditions'; import Salesforce from '@/_components/Salesforce'; -import ToolJetDbOperations from '@/Editor/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations'; +import ToolJetDbOperations from '@/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations'; import { orgEnvironmentVariableService, orgEnvironmentConstantService } from '../_services'; import { find, isEmpty } from 'lodash'; import { ButtonSolid } from './AppButton'; diff --git a/frontend/src/_ui/Accordion/AccordionItem.js b/frontend/src/_ui/Accordion/AccordionItem.js index 275bed5e6c..22fe54512d 100644 --- a/frontend/src/_ui/Accordion/AccordionItem.js +++ b/frontend/src/_ui/Accordion/AccordionItem.js @@ -31,17 +31,18 @@ const AccordionItem = ({ open = true, index, title, children }) => { } return (
-

setShow(!show)} className="accordion-header" id={`heading-${index}`}> - + +

{ + const darkMode = localStorage.getItem('darkMode') === 'true'; + const sortOptions = [ + { value: 'asc', label: 'Ascending' }, + { value: 'desc', label: 'Descending' }, + ]; + return ( +
+ {options.length === 0 && ( +
+ + There are no key value pairs added +
+ )} + {options.map((option, index) => { + return ( +
+
+
+ keyValuePairValueChanged(e.target.value, 0, index)} + /> +
+
+ keyValuePairValueChanged(e.target.value, 0, index)} + /> +