mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 01:18:23 +00:00
Merge pull request #11062 from ToolJet/fix/page-menu-and-import-export
page menu and import export changes
This commit is contained in:
commit
b4e1ab8a9d
30 changed files with 580 additions and 80 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export default function OverflowTooltip({ children, className, whiteSpace = 'now
|
|||
>
|
||||
<div
|
||||
ref={textElementRef}
|
||||
className={rest.childrenClassName}
|
||||
style={{
|
||||
whiteSpace,
|
||||
overflow: 'hidden',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,9 @@ function autoSaveApp(
|
|||
global_settings: {
|
||||
update: { ...diff },
|
||||
},
|
||||
page_settings: {
|
||||
update: { ...diff },
|
||||
},
|
||||
};
|
||||
|
||||
const body = !type
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@
|
|||
will-change: initial;
|
||||
}
|
||||
|
||||
|
||||
.PopoverArrow {
|
||||
fill: white;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
21
frontend/src/_ui/Icon/solidIcons/Reset.jsx
Normal file
21
frontend/src/_ui/Icon/solidIcons/Reset.jsx
Normal 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;
|
||||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
18
server/migrations/1716921638529-AddIconFieldToPagesTable.ts
Normal file
18
server/migrations/1716921638529-AddIconFieldToPagesTable.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -23,4 +23,7 @@ export class AppVersionUpdateDto {
|
|||
|
||||
@IsOptional()
|
||||
globalSettings: any;
|
||||
|
||||
@IsOptional()
|
||||
pageSettings: any;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export class CreatePageDto {
|
|||
disabled: boolean;
|
||||
|
||||
@IsOptional()
|
||||
hidden: boolean;
|
||||
hidden: Record<string, any>;
|
||||
|
||||
@IsOptional()
|
||||
autoComputeLayout: boolean;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue