Merge pull request #11062 from ToolJet/fix/page-menu-and-import-export

page menu and import export changes
This commit is contained in:
Johnson Cherian 2024-10-22 18:47:12 +05:30 committed by GitHub
commit b4e1ab8a9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 580 additions and 80 deletions

View file

@ -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) {

View file

@ -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 = ({
<div className="col tj-text-xsm p-0 color-slate12" data-cy={`${String(cyLabel)}-value`}>
{value}
</div>
{typeof onReset === 'function' && (
<div className="col-auto p-0">
<OverlayTrigger placement="left" overlay={<Tooltip id="reset-default-color">Reset to default</Tooltip>}>
<div onClick={onReset} className="color-reset">
<SolidIcon name="reset" />
</div>
</OverlayTrigger>
</div>
)}
</div>
);
};

View file

@ -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 (
<ToggleGroup.Item className="ToggleGroupItem" value={value} {...restProps}>
<ToggleGroup.Item className={`ToggleGroupItem ${className}`} value={value} {...restProps}>
{' '}
<div className="toggle-item" data-cy={`togglr-button-${value}`}>
{!isIcon ? (
children

View file

@ -24,6 +24,7 @@ export default function OverflowTooltip({ children, className, whiteSpace = 'now
>
<div
ref={textElementRef}
className={rest.childrenClassName}
style={{
whiteSpace,
overflow: 'hidden',

View file

@ -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
}

View file

@ -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 (
<DndContext
sensors={sensors}
onDragEnd={({ active, over }) => {
if (isVersionReleased) {
if (shouldFreeze) {
enableReleasedVersionPopupState();
return;
}

View file

@ -29,7 +29,7 @@ export function SortableItem({ children, id, classNames }) {
return (
<SortableItemContext.Provider value={context}>
<div className={classNames} ref={setNodeRef} style={style}>
<div {...attributes} {...listeners} ref={setNodeRef} className={classNames} style={style}>
{children}
</div>
</SortableItemContext.Provider>
@ -51,3 +51,16 @@ export function DragHandle({ show = true }) {
</button>
);
}
// hoc for wrapping components that need to be draggable
export function withDraggable(Component) {
return function DraggableComponent(props) {
const { attributes, listeners, ref } = useContext(SortableItemContext);
return (
<div {...attributes} {...listeners} ref={ref} className="draggable-container">
<Component {...props} />
</div>
);
};
}

View file

@ -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 (
<div className="d-flex justify-content-center align-items-center" style={{ height: '100%' }}>
<div>
<EmptyIllustration />
<p data-cy={`label-no-pages-found`} className="mt-3 color-slate12">
No pages found
</p>
</div>
</div>
);
}
return (
<div style={{ maxWidth: 400, margin: '0' }}>
<SortableList
items={items}
onChange={setItems}
items={pagesTorender}
onChange={reorderPages}
renderItem={(page) => (
<SortableList.Item id={page.id} classNames={restProps.classNames}>
<Element page={page} {...restProps} />

View file

@ -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);

View file

@ -93,6 +93,9 @@ function autoSaveApp(
global_settings: {
update: { ...diff },
},
page_settings: {
update: { ...diff },
},
};
const body = !type

View file

@ -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;

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -20,7 +20,6 @@
will-change: initial;
}
.PopoverArrow {
fill: white;
}

View file

@ -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,

View file

@ -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 && (
<div className="tj-list-item-icon">
@ -73,10 +84,24 @@ function FolderList({
</div>
)}
{CustomIcon && (
<div className="custom-icon">
<CustomIcon
color={computedStyles?.icon?.color}
style={{
width: '18px',
height: '18px',
color: computedStyles?.icon?.color,
stroke: computedStyles?.icon?.color,
}}
/>
</div>
)}
{children}
{RightIcon && <div className="tj-list-item-icon">{RightIcon && <SolidIcon name={RightIcon} />}</div>}
{overLayComponent && (isHovered || showGroupOptions) && (
{overLayComponent && ((!disableHoverOption && isHovered) || showGroupOptions) && (
<>
<div ref={target}>
<ButtonSolid
@ -87,7 +112,7 @@ function FolderList({
variant="tertiary"
onMouseEnter={handleMouseEnterInside}
onMouseLeave={handleMouseLeaveInside}
data-cy="groups-list-option-button"
dataCy={'groups-list-option-button'}
></ButtonSolid>
</div>
<Overlay
@ -114,6 +139,8 @@ function FolderList({
) : (
<Skeleton count={4} />
)}
<Tooltip id="button-content" place="right" style={{ zIndex: 99999, width: '150px' }} show={toolTipDisabled} />
</>
);
}

View file

@ -0,0 +1,21 @@
import React from 'react';
const Reset = ({ fill = '#6A727C', width = '10', className = '', viewBox = '0 0 10 10' }) => (
<svg
width={width}
height={width}
viewBox={viewBox}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.71935 1.71937C3.9983 1.44042 3.9983 0.988158 3.71935 0.70921C3.4404 0.430263 2.98814 0.430263 2.7092 0.70921L0.209197 3.20921C-0.0697323 3.48816 -0.0697323 3.94042 0.209197 4.21937L2.7092 6.71938C2.98814 6.99832 3.4404 6.99832 3.71935 6.71938C3.9983 6.44042 3.9983 5.98817 3.71935 5.70922L2.4387 4.42858H6.60714C7.69198 4.42858 8.57143 5.30802 8.57143 6.39287C8.57143 7.47773 7.69198 8.35716 6.60714 8.35716H4.99999C4.6055 8.35716 4.2857 8.67694 4.2857 9.07144C4.2857 9.46594 4.6055 9.78573 4.99999 9.78573H6.60714C8.48096 9.78573 10 8.26673 10 6.39287C10 4.51904 8.48096 3 6.60714 3H2.4387L3.71935 1.71937Z"
fill={fill}
/>
</svg>
);
export default Reset;

View file

@ -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 <RightOuterJoin {...props} />;
case 'row':
return <Row {...props} />;
case 'reset':
return <Reset {...props} />;
case 'sadrectangle':
return <SadRectangle {...props} />;
case 'search':

View file

@ -0,0 +1,38 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class MoveHiddenFieldInAppVersionsToPageSettings1718357264489 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
return Promise.resolve();
}
}

View file

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddPageSettingsColumnToAppVersionTable1716890766240 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'app_versions',
new TableColumn({
name: 'page_settings',
type: 'json',
isNullable: true,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('app_versions', 'page_settings');
}
}

View file

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddIconFieldToPagesTable1716921638529 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'pages',
new TableColumn({
name: 'icon',
type: 'varchar',
isNullable: true,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('pages', 'icon');
}
}

View file

@ -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')

View file

@ -23,4 +23,7 @@ export class AppVersionUpdateDto {
@IsOptional()
globalSettings: any;
@IsOptional()
pageSettings: any;
}

View file

@ -23,7 +23,7 @@ export class CreatePageDto {
disabled: boolean;
@IsOptional()
hidden: boolean;
hidden: Record<string, any>;
@IsOptional()
autoComputeLayout: boolean;

View file

@ -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;

View file

@ -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;

View file

@ -6,14 +6,32 @@ export function updateEntityReferences(node, resourceMapping: Record<string, str
const referenceExists = value;
if (referenceExists) {
const ref = value.replace('{{', '').replace('}}', '');
const matches = value.match(/{{(.*?)}}/g);
// gett all references {{entityName}}
if (matches) {
matches.forEach((match) => {
// 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);

View file

@ -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;
}

View file

@ -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);

View file

@ -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;
}