diff --git a/client/packages/lowcoder/index.html b/client/packages/lowcoder/index.html index f3019a0cd..b9f940e01 100644 --- a/client/packages/lowcoder/index.html +++ b/client/packages/lowcoder/index.html @@ -28,6 +28,8 @@ display: flex; pointer-events: none; flex-direction: column; + top: 0; + z-index: 10000; } #loading svg { animation: breath 1s linear infinite; diff --git a/client/packages/lowcoder/src/components/DraggableTree/DraggableItem.tsx b/client/packages/lowcoder/src/components/DraggableTree/DraggableItem.tsx index c8a0f093e..4d827381c 100644 --- a/client/packages/lowcoder/src/components/DraggableTree/DraggableItem.tsx +++ b/client/packages/lowcoder/src/components/DraggableTree/DraggableItem.tsx @@ -15,7 +15,7 @@ const Wrapper = styled.div<{ $itemHeight?: number; }>` position: relative; - width: 100%; + width: auto; height: ${(props) => props.$itemHeight ?? 30}px; /* border: 1px solid #d7d9e0; */ border-radius: 4px; diff --git a/client/packages/lowcoder/src/components/DraggableTree/DroppableMenuItem.tsx b/client/packages/lowcoder/src/components/DraggableTree/DroppableMenuItem.tsx index 68c355ec3..3d49e438a 100644 --- a/client/packages/lowcoder/src/components/DraggableTree/DroppableMenuItem.tsx +++ b/client/packages/lowcoder/src/components/DraggableTree/DroppableMenuItem.tsx @@ -6,6 +6,7 @@ import { DraggableTreeContext } from "./DraggableTreeContext"; import DroppablePlaceholder from "./DroppablePlaceHolder"; import { DraggableTreeNode, DraggableTreeNodeItemRenderProps, IDragData, IDropData } from "./types"; import { checkDroppableFlag } from "./util"; +import { Flex } from "antd"; const DraggableMenuItemWrapper = styled.div` position: relative; @@ -88,29 +89,34 @@ export default function DraggableMenuItem(props: IDraggableMenuItemProps) { disabled={isDragging || disabled} /> )} - { - setDragNodeRef(node); - setDropNodeRef(node); - }} - {...dragListeners} - > - {renderContent?.({ - node: item, - isOver, - path, - isOverlay, - hasChildren: items.length > 0, - dragging: !!(isDragging || parentDragging), - isFolded: isFold, - onDelete: () => onDelete?.(path), - onToggleFold: () => context.toggleFold(id), - }) || null} - + + { + setDragNodeRef(node); + setDropNodeRef(node); + }} + {...dragListeners} + > + + +
+ {renderContent?.({ + node: item, + isOver, + path, + isOverlay, + hasChildren: items.length > 0, + dragging: !!(isDragging || parentDragging), + isFolded: isFold, + onDelete: () => onDelete?.(path), + onToggleFold: () => context.toggleFold(id), + }) || null} +
+
{items.length > 0 && !isFold && (
diff --git a/client/packages/lowcoder/src/comps/comps/rootComp.tsx b/client/packages/lowcoder/src/comps/comps/rootComp.tsx index 5fede0b07..83fe577c9 100644 --- a/client/packages/lowcoder/src/comps/comps/rootComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/rootComp.tsx @@ -34,7 +34,7 @@ import { ExternalEditorContext } from "util/context/ExternalEditorContext"; import { useUserViewMode } from "util/hooks"; import React from "react"; import { isEqual } from "lodash"; - +import {LoadingBarHideTrigger} from "@lowcoder-ee/util/hideLoading"; const EditorView = lazy( () => import("pages/editor/editorView"), ); @@ -138,6 +138,7 @@ const RootView = React.memo((props: RootViewProps) => {
{comp.children.queries.children[key].getView()}
))} + diff --git a/client/packages/lowcoder/src/constants/reduxActionConstants.ts b/client/packages/lowcoder/src/constants/reduxActionConstants.ts index be3cd6271..6df5991f2 100644 --- a/client/packages/lowcoder/src/constants/reduxActionConstants.ts +++ b/client/packages/lowcoder/src/constants/reduxActionConstants.ts @@ -9,7 +9,7 @@ export const ReduxActionTypes = { FETCH_RAW_CURRENT_USER_SUCCESS: "FETCH_RAW_CURRENT_USER_SUCCESS", FETCH_API_KEYS: "FETCH_API_KEYS", FETCH_API_KEYS_SUCCESS: "FETCH_API_KEYS_SUCCESS", - + MOVE_TO_FOLDER2_SUCCESS: "MOVE_TO_FOLDER2_SUCCESS", /* plugin RELATED */ FETCH_DATA_SOURCE_TYPES: "FETCH_DATA_SOURCE_TYPES", diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index a4672903e..999890d01 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -2785,6 +2785,7 @@ export const en = { "switch": "Switch Component: " }, "module": { + "folderNotEmpty": "Folder is not empty", "emptyText": "No Data", "docLink": "Read More About Modules...", "documentationText" : "Modules are complete Applications, that can get included and repeated in other Applications and it functions just like a single component. As modules can get embedded, they need to be able to interact with your outside apps or websites. This four settings help to support communication with a Module.", diff --git a/client/packages/lowcoder/src/index.ts b/client/packages/lowcoder/src/index.ts index 086d19d0e..2072fc849 100644 --- a/client/packages/lowcoder/src/index.ts +++ b/client/packages/lowcoder/src/index.ts @@ -24,7 +24,7 @@ if (!window.ResizeObserver) { window.ResizeObserver = ResizeObserver; } -function hideLoading() { +export function hideLoading() { // hide loading const node = document.getElementById("loading"); if (node) { @@ -42,7 +42,7 @@ debug(`REACT_APP_LOG_LEVEL:, ${REACT_APP_LOG_LEVEL}`); try { bootstrap(); - hideLoading(); + // hideLoading(); } catch (e) { log.error(e); } diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx index c6fd5f91f..fc2f7536a 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx @@ -73,7 +73,7 @@ import AppEditor from "../editor/AppEditor"; import { fetchDeploymentIdAction } from "@lowcoder-ee/redux/reduxActions/configActions"; import { getDeploymentId } from "@lowcoder-ee/redux/selectors/configSelectors"; import { SimpleSubscriptionContextProvider } from '@lowcoder-ee/util/context/SimpleSubscriptionContext'; - +import {LoadingBarHideTrigger} from "@lowcoder-ee/util/hideLoading"; const TabLabel = styled.div` font-weight: 500; `; @@ -222,6 +222,7 @@ export default function ApplicationHome() { return ( + string + checkName: (val: string) => string +}; + + + +function buildTree(elementRecord: Record>): NodeType { + const elements = elementRecord[""]; + const elementMap: Record = {}; + let rootNode: NodeType = { + name: "root", + id: "", + isFolder: true, + children: [], + rename: val => rootNode.name = val, + checkName: val => val } - &:hover { - cursor: grab; - .module-icon { - box-shadow: 0 0 5px 0 rgba(49, 94, 251, 0.15); - border-color: ${BorderActiveShadowColor}; - transform: scale(1.2); - } - .module-name { - color: ${ActiveTextColor}; + + // Initialize all folders and applications as NodeType + for (const element of elements) { + if (element.folder) { + elementMap[element.folderId] = { + name: element.name, + id: element.folderId, + isFolder: true, + children: [], + rename: val => elementMap[element.folderId].name = val, + checkName: val => val + }; + + // Process subapplications inside the folder + for (const app of element.subApplications || []) { + if (!!app && app.applicationType === AppTypeEnum.Module) { + const appNode: NodeType = { + name: app.name, + id: app.applicationId, + containerSize: app.containerSize, + isFolder: false, + children: [], + module: app, + rename: val => appNode.name = val, + checkName: val => val + }; + elementMap[element.folderId].children.push(appNode); // Add applications as children of the folder + } + } + } else { + if (element.applicationType === AppTypeEnum.Module) { + elementMap[element.applicationId] = { + name: element.name, + containerSize: element.containerSize, + id: element.applicationId, + isFolder: false, + children: [], + module: element, + rename: val => elementMap[element.applicationId].name = val, + checkName: val => val + }; + } } } - .module-icon { - transition: all 200ms linear; - margin-right: 8px; - width: 40px; - height: 40px; - display: flex; - justify-content: center; - align-items: center; - border: 1px solid ${BorderColor}; - border-radius: 4px; - } - .module-content { - flex: 1; - display: flex; - flex-direction: column; - justify-content: space-around; - overflow: hidden; - } - .module-name { - line-height: 1.5; - font-size: 13px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - .module-desc { - line-height: 1.5; - font-size: 12px; - color: ${GreyTextColor}; + + // Build the tree structure + for (const element of elements) { + if (element.folder) { + const parentId = element.parentFolderId; + if (parentId && elementMap[parentId]) { + elementMap[parentId].children.push(elementMap[element.folderId]); + } else { + rootNode.children.push(elementMap[element.folderId]); + } + } else if (elementMap[element.applicationId]) { + rootNode.children.push(elementMap[element.applicationId]); + } } -`; + rootNode.children.sort((a, b) => { + if (a.isFolder && !b.isFolder) { + return -1; // a is a isFolder and should come first + } else if (!a.isFolder && b.isFolder) { + return 1; // b is a folder and should come first + } else { + return 0; // both are folders or both are not, keep original order + } + }); + return rootNode; +} + interface ModuleItemProps { meta: ApplicationMeta; onDrag: (type: string) => void; + isOverlay: boolean; + selectedID: string; + setSelectedID: (id: string) => void; + selectedType: boolean; + setSelectedType: (id: boolean) => void; + resComp: NodeType; + id: string; } function ModuleItem(props: ModuleItemProps) { const compType = "module"; - const { meta } = props; + const { + meta , + isOverlay, + selectedID, + setSelectedID, + selectedType, + setSelectedType, + resComp, + id + } = props; + const dispatch = useDispatch(); + const type = resComp.isFolder; + const name = resComp.name; + const [error, setError] = useState(undefined); + const [editing, setEditing] = useState(false); + const readOnly = useSelector(showAppSnapshotSelector); + const isSelected = type === selectedType && id === selectedID; + const handleFinishRename = (value: string) => { + if (value !== "") { + let success = false; + let compId = name; + if (resComp.rename) { + compId = resComp.rename(value); + success = !!compId; + } else { + compId = name; + success = true; + } + if (success) { + setSelectedID(compId); + setSelectedType(type); + setError(undefined); + try { + dispatch(updateAppMetaAction({ + applicationId: selectedID, + name: value + })); + } catch (error) { + console.error("Error: Rename module in extension:", error); + throw error; + } + } + setError(undefined); + } + setError(undefined); + }; + + const handleNameChange = (value: string) => { + value === "" ? setError("Cannot Be Empty") : setError(undefined); + }; return ( { + e.stopPropagation(); e.dataTransfer.setData("compType", compType); e.dataTransfer.setDragImage(TransparentImg, 0, 0); draggingUtils.setData("compType", compType); @@ -99,58 +241,501 @@ function ModuleItem(props: ModuleItemProps) { props.onDrag(compType); }} > -
- -
-
-
{props.meta.name}
-
{formatTimestamp(props.meta.createAt)}
+
+ +
+ setEditing(editing)} + /> + +
); } +const HighlightBorder = styled.div<{ $active: boolean; $foldable: boolean; $level: number }>` + max-width: 100%; + flex: 1; + display: flex; + padding-left: ${(props) => props.$level * 20 + (props.$foldable ? 0 : 14)}px; + border-radius: 4px; + border: 1px solid ${(props) => (props.$active ? BorderActiveColor : "transparent")}; + align-items: center; + justify-content: space-between; +`; + +interface ColumnDivProps { + $color?: boolean; + $isOverlay: boolean; +} + +const ColumnDiv = styled.div` + width: 100%; + height: 25px; + display: flex; + user-select: none; + padding-left: 2px; + padding-right: 15px; + background-color: ${(props) => (props.$isOverlay ? "rgba(255, 255, 255, 0.11)" : "")}; + + &&& { + background-color: ${(props) => (props.$color && !props.$isOverlay ? "#f2f7fc" : null)}; + } + + &:hover { + background-color: #f2f7fc80; + cursor: pointer; + } + + .taco-edit-text-wrapper { + width: 100%; + height: 21px; + line-height: 21px; + color: #222222; + margin-left: 0; + font-size: 13px; + padding-left: 0; + + &:hover { + background-color: transparent; + } + } + + .taco-edit-text-input { + width: 100%; + height: 21px; + line-height: 21px; + color: #222222; + margin-left: 0; + font-size: 13px; + background-color: #fdfdfd; + border: 1px solid #3377ff; + border-radius: 2px; + + &:focus { + border-color: #3377ff; + box-shadow: 0 0 0 2px #d6e4ff; + } + } +`; + +const FoldIconBtn = styled.div` + width: 12px; + height: 12px; + display: flex; + margin-right: 2px; +`; + +const Icon = styled(PointIcon)` + width: 16px; + height: 16px; + cursor: pointer; + flex-shrink: 0; + color: ${NormalMenuIconColor}; + + &:hover { + color: #315efb; + } +`; + +interface ModuleSidebarItemProps extends DraggableTreeNodeItemRenderProps { + id: string; + resComp: NodeType; + onCopy: () => void; + onSelect: () => void; + onDelete: () => void; + onToggleFold: () => void; + selectedID: string; + setSelectedID: (id: string) => void; + selectedType: boolean; + setSelectedType: (id: boolean) => void; +} + +const empty = ( + +

{trans("rightPanel.emptyModules")}

+ { + const appId = app.applicationInfoView.applicationId; + const url = APPLICATION_VIEW_URL(appId, "edit"); + window.open(url); + }} + /> + + } + /> +); + +function ModuleSidebarItem(props: ModuleSidebarItemProps) { + const dispatch = useDispatch(); + const { + id, + resComp, + isOver, + isOverlay, + path, + isFolded, + selectedID, + setSelectedID, + selectedType, + setSelectedType, + onDelete, + onCopy, + onSelect, + onToggleFold, + } = props; + const { onDrag } = useContext(RightContext); + const [error, setError] = useState(undefined); + const [editing, setEditing] = useState(false); + const readOnly = useSelector(showAppSnapshotSelector); + const level = path.length - 1; + const type = resComp.isFolder; + const name = resComp.name; + const isSelected = type === selectedType && id === selectedID; + const isFolder = type; + + const handleFinishRename = (value: string) => { + if (value !== ""){ + let success = false; + let compId = name; + if (resComp.rename) { + compId = resComp.rename(value); + success = !!compId; + } else { + compId = name; + success = true; + } + if (success) { + setSelectedID(compId); + setSelectedType(type); + setError(undefined); + try{ + dispatch(updateFolder({ id: selectedID, name: value })); + } catch (error) { + console.error("Error: Delete module in extension:", error); + throw error; + } + + } + setError(undefined); + } + }; + + const handleNameChange = (value: string) => { + value === "" ? setError("Cannot Be Empty") : setError(undefined); + }; + + const handleClickItem = () => { + if (isFolder) { + onToggleFold(); + } + onSelect(); + }; + + return ( + + + {isFolder && {!isFolded ? : }} + { isFolder ? + <> + +
+ setEditing(editing)} + /> + +
+ : + } + {!readOnly && !isOverlay && ( + onDelete()}> + + + )} +
+
+ ); +} + export default function ModulePanel() { const dispatch = useDispatch(); - const modules = useSelector(modulesSelector); - const { onDrag, searchValue } = useContext(RightContext); - const { applicationId } = useContext(ExternalEditorContext); + let elements = useSelector(folderElementsSelector); + const { searchValue } = useContext(RightContext); + const [selectedID, setSelectedID] = useState(""); + const [selectedType, setSelectedType] = useState(false); + let sourceFolderId : string = ""; + let sourceId : string = ""; + let folderId : string = ""; + const tree = buildTree(elements); + const getById = (id: string): NodeType | undefined => getByIdFromNode(tree, id); + let popedItemSourceId = ""; useEffect(() => { dispatch(fetchAllModules({})); }, [dispatch]); - const filteredModules = modules.filter((i) => { - if (i.applicationId === applicationId || i.applicationType !== AppTypeEnum.Module) { - return false; + const moveModule = () => { + try{ + if (sourceId !== "") { + dispatch( + moveToFolder( + { + sourceFolderId: sourceFolderId!, + sourceId: sourceId!, + folderId: folderId!, + moveFlag: true + }, + () => { + + + }, + () => {} + ) + ); + } + } catch (error) { + console.error("Error: Move module in extension:", error); + throw error; + } finally { + folderId = ""; + sourceId = ""; + sourceFolderId = ""; + } + + } + + const getByIdFromNode = (root: NodeType | null, id: string): NodeType | undefined => { + if (!root) { + return; } - return i.name?.toLowerCase()?.includes(searchValue.trim()?.toLowerCase()) || !searchValue?.trim(); - }); - const items = filteredModules.map((i) => ( - - )); - const empty = ( - -

{trans("rightPanel.emptyModules")}

- { - const appId = app.applicationInfoView.applicationId; - const url = APPLICATION_VIEW_URL(appId, "edit"); - window.open(url); - }} - /> - + if (root.id === id) { + return root; + } + + for (const child of root.children) { + const result = getByIdFromNode(child, id); + if (result) { + return result; } - /> - ); + } + return; + } + const convertRefTree = (treeNode: NodeType) => { //Convert elements into tree + const moduleResComp = getById(treeNode.id); + const currentNodeType = moduleResComp?.isFolder; + + const childrenItems = treeNode.children + .map((i) => convertRefTree(i as NodeType)) + .filter((i): i is DraggableTreeNode => !!i); + const node: DraggableTreeNode = { + id: moduleResComp?.id, + canDropBefore: (source) => { + if (currentNodeType) { + return source?.isFolder!; + } + + return !source?.isFolder; + }, + canDropAfter: (source) => { + if ( + !currentNodeType && + source?.isFolder + ) { + return false; + } + return true; + }, + canDropIn: (source) => { + if (!currentNodeType) { + return false; + } + if (!source) { + return true; + } + if (source.isFolder) { + return false; + } + return true; + }, + items: childrenItems, + data: moduleResComp, + addSubItem(value) { + folderId = node.id!; + moveModule(); + }, + deleteItem(index) { + sourceFolderId = node.id!; + sourceId = node.items[index].id!; + + }, + addItem(value) { + folderId = node.id!; + moveModule(); + }, + moveItem(from, to) { + }, + }; + + if ( + searchValue && + moduleResComp && + !moduleResComp.name.toLowerCase().includes(searchValue.toLowerCase()) && + childrenItems.length === 0 + ) { + return; + } + return node; + }; + const node = convertRefTree(tree); + function onCopy(type: boolean, id: string) { + } + + function onSelect(type: boolean, id: string, meta: any) { + setSelectedID(id); + setSelectedType(type); + } + + function onDelete(type: boolean, id: string, node: NodeType) { + if (type) { + if (node.children.length) { + messageInstance.error(trans("module.folderNotEmpty")) + } else { + try { + dispatch( + deleteFolder( + {folderId: id, parentFolderId: ""}, + () => { + messageInstance.success(trans("home.deleteSuccessMsg")); + }, + () => { + messageInstance.error(trans("error")) + } + ) + ); + } catch (error) { + console.error("Error: Remove folder in extension:", error); + throw error; + } + } + } else { + try { + CustomModal.confirm({ + title: trans("home.moveToTrash"), + content: transToNode("home.moveToTrashSubTitle", { + type: "", + name: "This file", + }), + onConfirm: () => { + dispatch( + recycleApplication( + { + applicationId: id, + folderId: popedItemSourceId, + }, + () => { + messageInstance.success(trans("success")); + + }, + () => { + messageInstance.error(trans("error")); + } + ) + ) + }, + confirmBtnType: "delete", + okText: trans("home.moveToTrash"), + onCancel: () => {} + }); + } catch (error) { + console.error("Error: Remove module in extension:", error); + throw error; + } + } + } return ( <> {trans("rightPanel.moduleListTitle")} - {items.length > 0 ? items : empty} + {node?.items.length ? + node={node!} + disable={!!searchValue} + unfoldAll={!!searchValue} + showSubInDragOverlay={false} + showDropInPositionLine={false} + showPositionLineDot + positionLineDotDiameter={4} + positionLineHeight={1} + itemHeight={25} + positionLineIndent={(path, dropInAsSub) => { + const indent = 2 + (path.length - 1) * 30; + if (dropInAsSub) { + return indent + 12; + } + return indent; + }} + renderItemContent={(params) => { + const { node, onToggleFold, onDelete: onDeleteTreeItem, ...otherParams } = params; + const resComp = node.data; + if (!resComp) { + return null; + } + const id = resComp.id; + const isFolder = resComp.isFolder; + return ( + onCopy(isFolder, id)} + onSelect={() => onSelect(isFolder, id, resComp)} + selectedID={selectedID} + setSelectedID={setSelectedID} + selectedType={selectedType} + setSelectedType={setSelectedType} + onDelete={() => { + (onDelete(isFolder, id, resComp)) + }} + {...otherParams} + /> + ); + }} + /> : empty} ); } diff --git a/client/packages/lowcoder/src/pages/userAuth/index.tsx b/client/packages/lowcoder/src/pages/userAuth/index.tsx index 40e7a1bc1..d33b48fde 100644 --- a/client/packages/lowcoder/src/pages/userAuth/index.tsx +++ b/client/packages/lowcoder/src/pages/userAuth/index.tsx @@ -11,7 +11,7 @@ import { fetchConfigAction } from "redux/reduxActions/configActions"; import { fetchUserAction } from "redux/reduxActions/userActions"; import LoginAdmin from "./loginAdmin"; import _ from "lodash"; - +import {LoadingBarHideTrigger} from "@lowcoder-ee/util/hideLoading"; export default function UserAuth() { const dispatch = useDispatch(); const location = useLocation(); @@ -50,6 +50,7 @@ export default function UserAuth() { fetchUserAfterAuthSuccess, }} > + diff --git a/client/packages/lowcoder/src/redux/reducers/uiReducers/folderReducer.ts b/client/packages/lowcoder/src/redux/reducers/uiReducers/folderReducer.ts index c27cb8d50..4326cb2ec 100644 --- a/client/packages/lowcoder/src/redux/reducers/uiReducers/folderReducer.ts +++ b/client/packages/lowcoder/src/redux/reducers/uiReducers/folderReducer.ts @@ -37,10 +37,24 @@ export const folderReducer = createReducer(initialState, { state: FolderReduxState, action: ReduxAction ): FolderReduxState => { + const deleteArray : number[] = []; const elements = { ...state.folderElements }; - elements[action.payload.folderId ?? ""] = elements[action.payload.folderId ?? ""]?.filter( - (e) => e.folder || (!e.folder && e.applicationId !== action.payload.applicationId) - ); + elements[""] = elements[""].map((item, index) => { + if(item.folder) { + const tempSubApplications = item.subApplications?.filter(e => e.applicationId !== action.payload.applicationId); + return { ...item, subApplications: tempSubApplications }; + } else { + if (item.applicationId !== action.payload.applicationId) + return item; + else { + deleteArray.push(index); + return item; + } + } + }); + deleteArray.map(item => { + elements[""].splice(item, 1); + }) return { ...state, folderElements: elements, @@ -55,6 +69,14 @@ export const folderReducer = createReducer(initialState, { elements[action.payload.folderId ?? ""] = elements[action.payload.folderId ?? ""]?.map((e) => { if (!e.folder && e.applicationId === action.payload.applicationId) { return { ...e, ...action.payload }; + } else { + if (e.folder) { + if (e.subApplications?.map(item => { + if (item.applicationId === action.payload.applicationId) + item.name = action.payload.name + })){ + } + } } return e; }); @@ -88,7 +110,7 @@ export const folderReducer = createReducer(initialState, { action.payload.parentFolderId ?? "" ]?.map((e) => { if (e.folder && e.folderId === action.payload.folderId) { - return { ...action.payload, name: action.payload.name }; + return { ...e, name: action.payload.name}; } return e; }); @@ -107,7 +129,7 @@ export const folderReducer = createReducer(initialState, { state: FolderReduxState, action: ReduxAction ): FolderReduxState => { - const elements = { ...state.folderElements }; + let elements = { ...state.folderElements }; elements[action.payload.sourceFolderId ?? ""] = elements[ action.payload.sourceFolderId ?? "" ]?.filter( @@ -120,6 +142,59 @@ export const folderReducer = createReducer(initialState, { folderElements: elements, }; }, + [ReduxActionTypes.MOVE_TO_FOLDER2_SUCCESS]: ( + state: FolderReduxState, + action: ReduxAction + ): FolderReduxState => { + let elements = { ...state.folderElements }; + const { sourceId, folderId, sourceFolderId } = action.payload; + if(sourceFolderId === "") { + const tempItem = elements[""]?.find(e => + !e.folder && e.applicationId === sourceId + ); + elements[""] = elements[""]?.filter(e => e.folder || (e.applicationId !== sourceId)); + elements[""] = elements[""].map(item => { + if(item.folder && item.folderId === folderId && tempItem !== undefined && !tempItem.folder) { + item.subApplications?.push(tempItem); + } + return item; + }) + } else{ + let tempIndex: number | undefined; + let tempNode: any; + let temp = elements[""].map((item, index) => { + if (item.folderId === sourceFolderId && item.folder) { + const tempSubApplications = item.subApplications?.filter(e => + (e.folder && e.folderId !== sourceId) || + (!e.folder && e.applicationId !== sourceId) + ); + tempNode = item.subApplications?.filter(e => + (e.folder && e.folderId === sourceId) || + (!e.folder && e.applicationId === sourceId) + ); + return { ...item, subApplications: tempSubApplications }; + } + if (item.folderId === folderId && item.folder) { + tempIndex = index; + return item; + } + return item; + }); + if (tempIndex !== undefined) { + const targetItem = temp[tempIndex]; + if (targetItem.folder && Array.isArray(targetItem.subApplications)) { + targetItem.subApplications.push(tempNode[0]); + } + } else { + temp.push(tempNode[0]); + } + elements[""] = temp; + } + return { + ...state, + folderElements: elements, + }; + }, [ReduxActionTypes.DELETE_FOLDER_SUCCESS]: ( state: FolderReduxState, action: ReduxAction diff --git a/client/packages/lowcoder/src/redux/reduxActions/folderActions.ts b/client/packages/lowcoder/src/redux/reduxActions/folderActions.ts index ba288b89a..5c00aafe6 100644 --- a/client/packages/lowcoder/src/redux/reduxActions/folderActions.ts +++ b/client/packages/lowcoder/src/redux/reduxActions/folderActions.ts @@ -58,6 +58,7 @@ export interface MoveToFolderPayload { sourceFolderId: string; sourceId: string; folderId: string; + moveFlag?: boolean; } export const moveToFolder = ( diff --git a/client/packages/lowcoder/src/redux/sagas/folderSagas.ts b/client/packages/lowcoder/src/redux/sagas/folderSagas.ts index 65a39f030..62b74659e 100644 --- a/client/packages/lowcoder/src/redux/sagas/folderSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/folderSagas.ts @@ -84,14 +84,16 @@ export function* deleteFolderSaga(action: ReduxActionWithCallbacks) { try { + const { moveFlag } = action.payload; + delete action.payload.moveFlag; const response: AxiosResponse> = yield FolderApi.moveToFolder( action.payload ); const isValidResponse: boolean = validateResponse(response); - + const type = moveFlag ? ReduxActionTypes.MOVE_TO_FOLDER2_SUCCESS : ReduxActionTypes.MOVE_TO_FOLDER_SUCCESS; if (isValidResponse) { yield put({ - type: ReduxActionTypes.MOVE_TO_FOLDER_SUCCESS, + type, payload: action.payload, }); action.onSuccessCallback && action.onSuccessCallback(response); diff --git a/client/packages/lowcoder/src/util/hideLoading.tsx b/client/packages/lowcoder/src/util/hideLoading.tsx new file mode 100644 index 000000000..f4c12c345 --- /dev/null +++ b/client/packages/lowcoder/src/util/hideLoading.tsx @@ -0,0 +1,10 @@ +import {useEffect} from "react"; +import {hideLoading} from "@lowcoder-ee/index"; + +export const LoadingBarHideTrigger = function(props: any) { + useEffect(() => { + setTimeout(() => hideLoading(), 300); + }, []); + + return <> +}; \ No newline at end of file diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 2c2bc5c27..5ecbbd579 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -17,6 +17,7 @@ COPY server/api-service/lowcoder-server/src/main/resources/application.yaml /low # Add bootstrapfile COPY deploy/docker/api-service/entrypoint.sh /lowcoder/api-service/entrypoint.sh COPY deploy/docker/api-service/init.sh /lowcoder/api-service/init.sh +ENV JAVA_OPTS="-Xmx2G -Xms512M" RUN chmod +x /lowcoder/api-service/*.sh ## diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistoryArchivedSnapshotRepository.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistoryArchivedSnapshotRepository.java index dded29c35..548d6e439 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistoryArchivedSnapshotRepository.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistoryArchivedSnapshotRepository.java @@ -1,6 +1,6 @@ package org.lowcoder.domain.application.repository; -import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; @@ -11,7 +11,7 @@ import java.time.Instant; @Repository -public interface ApplicationHistoryArchivedSnapshotRepository extends ReactiveMongoRepository { +public interface ApplicationHistoryArchivedSnapshotRepository extends ReactiveMongoRepository { @Query(value = "{ 'applicationId': ?0, $and: [" + "{$or: [ { 'context.operations': { $elemMatch: { 'compName': ?1 } } }, { $expr: { $eq: [?1, null] } } ]}, " + @@ -20,7 +20,7 @@ public interface ApplicationHistoryArchivedSnapshotRepository extends ReactiveMo "{$or: [ { 'createdAt': { $lte: ?4} }, { $expr: { $eq: [?4, null] } } ] } " + "]}", fields = "{applicationId : 1, context: 1, createdBy : 1, createdAt : 1}") - Flux findAllByApplicationId(String applicationId, String compName, String theme, Instant createdAtFrom, Instant createdAtTo, Pageable pageable); + Flux findAllByApplicationId(String applicationId, String compName, String theme, Instant createdAtFrom, Instant createdAtTo, Pageable pageable); Mono countByApplicationId(String applicationId); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistorySnapshotRepository.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistorySnapshotRepository.java index 809decfd6..eabf2caf6 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistorySnapshotRepository.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/repository/ApplicationHistorySnapshotRepository.java @@ -1,6 +1,6 @@ package org.lowcoder.domain.application.repository; -import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; @@ -11,7 +11,7 @@ import java.time.Instant; @Repository -public interface ApplicationHistorySnapshotRepository extends ReactiveMongoRepository { +public interface ApplicationHistorySnapshotRepository extends ReactiveMongoRepository { @Query(value = "{ 'applicationId': ?0, $and: [" + "{$or: [ { 'context.operations': { $elemMatch: { 'compName': ?1 } } }, { $expr: { $eq: [?1, null] } } ]}, " + @@ -20,7 +20,7 @@ public interface ApplicationHistorySnapshotRepository extends ReactiveMongoRepos "{$or: [ { 'createdAt': { $lte: ?4} }, { $expr: { $eq: [?4, null] } } ] } " + "]}", fields = "{applicationId : 1, context: 1, createdBy : 1, createdAt : 1}") - Flux findAllByApplicationId(String applicationId, String compName, String theme, Instant createdAtFrom, Instant createdAtTo, Pageable pageable); + Flux findAllByApplicationId(String applicationId, String compName, String theme, Instant createdAtFrom, Instant createdAtTo, Pageable pageable); Mono countByApplicationId(String applicationId); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationHistorySnapshotService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationHistorySnapshotService.java index fd4a79f82..f4e5b3fcf 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationHistorySnapshotService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/ApplicationHistorySnapshotService.java @@ -13,12 +13,13 @@ public interface ApplicationHistorySnapshotService { Mono createHistorySnapshot(String applicationId, Map dsl, Map context, String userId); - Mono> listAllHistorySnapshotBriefInfo(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest); - Mono> listAllHistorySnapshotBriefInfoArchived(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest); + Mono> listAllHistorySnapshotBriefInfo(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest); + Mono> listAllHistorySnapshotBriefInfoArchived(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest); Mono countByApplicationId(String applicationId); + Mono countByApplicationIdArchived(String applicationId); - Mono getHistorySnapshotDetail(String historySnapshotId); + Mono getHistorySnapshotDetail(String historySnapshotId); - Mono getHistorySnapshotDetailArchived(String historySnapshotId); + Mono getHistorySnapshotDetailArchived(String historySnapshotId); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/impl/ApplicationHistorySnapshotServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/impl/ApplicationHistorySnapshotServiceImpl.java index c47b39955..2d4aba44a 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/impl/ApplicationHistorySnapshotServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/application/service/impl/ApplicationHistorySnapshotServiceImpl.java @@ -29,24 +29,24 @@ public class ApplicationHistorySnapshotServiceImpl implements ApplicationHistory @Override public Mono createHistorySnapshot(String applicationId, Map dsl, Map context, String userId) { - ApplicationHistorySnapshotTS applicationHistorySnapshotTS = new ApplicationHistorySnapshotTS(); - applicationHistorySnapshotTS.setApplicationId(applicationId); - applicationHistorySnapshotTS.setDsl(dsl); - applicationHistorySnapshotTS.setContext(context); - return repository.save(applicationHistorySnapshotTS) + ApplicationHistorySnapshot applicationHistorySnapshot = new ApplicationHistorySnapshot(); + applicationHistorySnapshot.setApplicationId(applicationId); + applicationHistorySnapshot.setDsl(dsl); + applicationHistorySnapshot.setContext(context); + return repository.save(applicationHistorySnapshot) .thenReturn(true) .onErrorReturn(false); } @Override - public Mono> listAllHistorySnapshotBriefInfo(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest) { + public Mono> listAllHistorySnapshotBriefInfo(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest) { return repository.findAllByApplicationId(applicationId, compName, theme, from, to, pageRequest.withSort(Direction.DESC, "id")) .collectList() .onErrorMap(Exception.class, e -> ofException(BizError.FETCH_HISTORY_SNAPSHOT_FAILURE, "FETCH_HISTORY_SNAPSHOT_FAILURE")); } @Override - public Mono> listAllHistorySnapshotBriefInfoArchived(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest) { + public Mono> listAllHistorySnapshotBriefInfoArchived(String applicationId, String compName, String theme, Instant from, Instant to, PageRequest pageRequest) { return repositoryArchived.findAllByApplicationId(applicationId, compName, theme, from, to, pageRequest.withSort(Direction.DESC, "id")) .collectList() .onErrorMap(Exception.class, e -> ofException(BizError.FETCH_HISTORY_SNAPSHOT_FAILURE, "FETCH_HISTORY_SNAPSHOT_FAILURE")); @@ -59,16 +59,23 @@ public Mono countByApplicationId(String applicationId) { e -> ofException(BizError.FETCH_HISTORY_SNAPSHOT_COUNT_FAILURE, "FETCH_HISTORY_SNAPSHOT_COUNT_FAILURE")); } + @Override + public Mono countByApplicationIdArchived(String applicationId) { + return repositoryArchived.countByApplicationId(applicationId) + .onErrorMap(Exception.class, + e -> ofException(BizError.FETCH_HISTORY_SNAPSHOT_COUNT_FAILURE, "FETCH_HISTORY_SNAPSHOT_COUNT_FAILURE")); + } + @Override - public Mono getHistorySnapshotDetail(String historySnapshotId) { + public Mono getHistorySnapshotDetail(String historySnapshotId) { return repository.findById(historySnapshotId) .switchIfEmpty(deferredError(INVALID_HISTORY_SNAPSHOT, "INVALID_HISTORY_SNAPSHOT", historySnapshotId)); } @Override - public Mono getHistorySnapshotDetailArchived(String historySnapshotId) { + public Mono getHistorySnapshotDetailArchived(String historySnapshotId) { return repositoryArchived.findById(historySnapshotId) .switchIfEmpty(deferredError(INVALID_HISTORY_SNAPSHOT, "INVALID_HISTORY_SNAPSHOT", historySnapshotId)); } diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java index 6b800720c..981000caf 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/user/service/UserServiceImpl.java @@ -418,7 +418,7 @@ protected Map convertConnections(Set connections) { return connections.stream() .filter(connection -> !AuthSourceConstants.EMAIL.equals(connection.getSource()) && !AuthSourceConstants.PHONE.equals(connection.getSource())) - .collect(Collectors.toMap(Connection::getSource, Connection::getRawUserInfo)); + .collect(Collectors.toMap(Connection::getAuthId, Connection::getRawUserInfo)); } protected String convertEmail(Set connections) { diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java index ed7079598..3c91f7ce7 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationController.java @@ -162,12 +162,12 @@ public Mono>> getApplications(@RequestPar @RequestParam(required = false) ApplicationStatus applicationStatus, @RequestParam(defaultValue = "true") boolean withContainerSize, @RequestParam(required = false) String name, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize) { ApplicationType applicationTypeEnum = applicationType == null ? null : ApplicationType.fromValue(applicationType); var flux = userHomeApiService.getAllAuthorisedApplications4CurrentOrgMember(applicationTypeEnum, applicationStatus, withContainerSize, name).cache(); Mono countMono = flux.count(); - var flux1 = flux.skip((long) pageNum * pageSize); + var flux1 = flux.skip((long) (pageNum - 1) * pageSize); if(pageSize > 0) flux1 = flux1.take(pageSize); return flux1.collectList().zipWith(countMono) .map(tuple -> PageResponseView.success(tuple.getT1(), pageNum, pageSize, Math.toIntExact(tuple.getT2()))); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java index 4eed69ee2..cef119847 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationEndpoints.java @@ -167,7 +167,7 @@ public Mono>> getApplications(@RequestPar @RequestParam(required = false) ApplicationStatus applicationStatus, @RequestParam(defaultValue = "true") boolean withContainerSize, @RequestParam(required = false) String name, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java index 6b6d94a51..b5a6381d7 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/application/ApplicationHistorySnapshotController.java @@ -13,6 +13,7 @@ import org.lowcoder.domain.application.service.ApplicationService; import org.lowcoder.domain.permission.model.ResourceAction; import org.lowcoder.domain.permission.service.ResourcePermissionService; +import org.lowcoder.domain.user.model.User; import org.lowcoder.domain.user.service.UserService; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -54,7 +55,7 @@ public Mono> create(@RequestBody ApplicationHistorySnapsho @Override public Mono>> listAllHistorySnapshotBriefInfo(@PathVariable String applicationId, - @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, @RequestParam String compName, @RequestParam String theme, @@ -69,15 +70,15 @@ public Mono>> listAllHistorySnapshotBriefInfo(@ .flatMap(__ -> applicationHistorySnapshotService.listAllHistorySnapshotBriefInfo(applicationId, compName, theme, from, to, pagination.toPageRequest())) .flatMap(snapshotList -> { Mono> snapshotBriefInfoList = multiBuild(snapshotList, - ApplicationHistorySnapshotTS::getCreatedBy, + ApplicationHistorySnapshot::getCreatedBy, userService::getByIds, - (applicationHistorySnapshotTS, user) -> new ApplicationHistorySnapshotBriefInfo( - applicationHistorySnapshotTS.getId(), - applicationHistorySnapshotTS.getContext(), - applicationHistorySnapshotTS.getCreatedBy(), + (applicationHistorySnapshot, user) -> new ApplicationHistorySnapshotBriefInfo( + applicationHistorySnapshot.getId(), + applicationHistorySnapshot.getContext(), + applicationHistorySnapshot.getCreatedBy(), user.getName(), user.getAvatarUrl(), - applicationHistorySnapshotTS.getCreatedAt().toEpochMilli() + applicationHistorySnapshot.getCreatedAt().toEpochMilli() ) ); @@ -91,7 +92,7 @@ public Mono>> listAllHistorySnapshotBriefInfo(@ @Override public Mono>> listAllHistorySnapshotBriefInfoArchived(@PathVariable String applicationId, - @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, @RequestParam String compName, @RequestParam String theme, @@ -106,19 +107,19 @@ public Mono>> listAllHistorySnapshotBriefInfoAr .flatMap(__ -> applicationHistorySnapshotService.listAllHistorySnapshotBriefInfoArchived(applicationId, compName, theme, from, to, pagination.toPageRequest())) .flatMap(snapshotList -> { Mono> snapshotBriefInfoList = multiBuild(snapshotList, - ApplicationHistorySnapshot::getCreatedBy, + ApplicationHistorySnapshotTS::getCreatedBy, userService::getByIds, - (applicationHistorySnapshot, user) -> new ApplicationHistorySnapshotBriefInfo( - applicationHistorySnapshot.getId(), - applicationHistorySnapshot.getContext(), - applicationHistorySnapshot.getCreatedBy(), + (applicationHistorySnapshotTS, user) -> new ApplicationHistorySnapshotBriefInfo( + applicationHistorySnapshotTS.getId(), + applicationHistorySnapshotTS.getContext(), + applicationHistorySnapshotTS.getCreatedBy(), user.getName(), user.getAvatarUrl(), - applicationHistorySnapshot.getCreatedAt().toEpochMilli() + applicationHistorySnapshotTS.getCreatedAt().toEpochMilli() ) ); - Mono applicationHistorySnapshotCount = applicationHistorySnapshotService.countByApplicationId(applicationId); + Mono applicationHistorySnapshotCount = applicationHistorySnapshotService.countByApplicationIdArchived(applicationId); return Mono.zip(snapshotBriefInfoList, applicationHistorySnapshotCount) .map(tuple -> ImmutableMap.of("list", tuple.getT1(), "count", tuple.getT2())); @@ -133,7 +134,7 @@ public Mono> getHistorySnapshotDsl(@PathVar .delayUntil(visitor -> resourcePermissionService.checkResourcePermissionWithError(visitor, applicationId, ResourceAction.EDIT_APPLICATIONS)) .flatMap(__ -> applicationHistorySnapshotService.getHistorySnapshotDetail(snapshotId)) - .map(ApplicationHistorySnapshotTS::getDsl) + .map(ApplicationHistorySnapshot::getDsl) .zipWhen(applicationService::getAllDependentModulesFromDsl) .map(tuple -> { Map applicationDsl = tuple.getT1(); @@ -155,7 +156,7 @@ public Mono> getHistorySnapshotDslArchived( .delayUntil(visitor -> resourcePermissionService.checkResourcePermissionWithError(visitor, applicationId, ResourceAction.EDIT_APPLICATIONS)) .flatMap(__ -> applicationHistorySnapshotService.getHistorySnapshotDetailArchived(snapshotId)) - .map(ApplicationHistorySnapshot::getDsl) + .map(ApplicationHistorySnapshotTS::getDsl) .zipWhen(applicationService::getAllDependentModulesFromDsl) .map(tuple -> { Map applicationDsl = tuple.getT1(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleController.java index 254e78037..cb0df9241 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleController.java @@ -106,7 +106,7 @@ public Mono>> getRecycledBundles() { @Override public Mono> getElements(@PathVariable String bundleId, @RequestParam(value = "applicationType", required = false) ApplicationType applicationType, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize) { String objectId = gidService.convertBundleIdToObjectId(bundleId); var flux = bundleApiService.getElements(objectId, applicationType).cache(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleEndpoints.java index 8674c62b5..8d668c1b7 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/bundle/BundleEndpoints.java @@ -123,7 +123,7 @@ public interface BundleEndpoints @GetMapping("/{bundleId}/elements") public Mono> getElements(@PathVariable String bundleId, @RequestParam(value = "applicationType", required = false) ApplicationType applicationType, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java index 31cf49494..24a77dd29 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderController.java @@ -70,12 +70,12 @@ public Mono> update(@RequestBody Folder folder) { public Mono> getElements(@RequestParam(value = "id", required = false) String folderId, @RequestParam(value = "applicationType", required = false) ApplicationType applicationType, @RequestParam(required = false) String name, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize) { String objectId = gidService.convertFolderIdToObjectId(folderId); var flux = folderApiService.getElements(objectId, applicationType, name).cache(); var countMono = flux.count(); - var flux1 = flux.skip((long) pageNum * pageSize); + var flux1 = flux.skip((long) (pageNum - 1) * pageSize); if(pageSize > 0) flux1 = flux1.take(pageSize); return flux1.collectList() .delayUntil(__ -> folderApiService.upsertLastViewTime(objectId)) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderEndpoints.java index 43e5ce785..2c6279084 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/home/FolderEndpoints.java @@ -71,7 +71,7 @@ public interface FolderEndpoints public Mono> getElements(@RequestParam(value = "id", required = false) String folderId, @RequestParam(value = "applicationType", required = false) ApplicationType applicationType, @RequestParam(required = false) String name, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java index d478bcfc2..4e7facb99 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupController.java @@ -75,7 +75,7 @@ public Mono> delete(@PathVariable String groupId) { } @Override - public Mono>> getOrgGroups(@RequestParam(required = false, defaultValue = "0") Integer pageNum, + public Mono>> getOrgGroups(@RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize) { return groupApiService.getGroups().flatMap(groupList -> { if(groupList.isEmpty()) return Mono.just(new GroupListResponseView<>(ResponseView.SUCCESS, @@ -99,7 +99,7 @@ public Mono>> getOrgGroups(@RequestParam(r .filter(orgMember -> !orgMember.isAdmin() && !orgMember.isSuperAdmin() && devMembers.stream().noneMatch(devMember -> devMember.getUserId().equals(orgMember.getUserId()))).toList().size(); - var subList = groupList.subList(pageNum * pageSize, pageSize <= 0?groupList.size():pageNum * pageSize + pageSize); + var subList = groupList.subList((pageNum - 1) * pageSize, pageSize <= 0?groupList.size():pageNum * pageSize); return new GroupListResponseView<>(ResponseView.SUCCESS, "", subList, diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java index 4f0825333..e2f8bfa7a 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/GroupEndpoints.java @@ -63,7 +63,7 @@ public Mono> update(@PathVariable String groupId, description = "Retrieve a list of User Groups within Lowcoder, providing an overview of available groups, based on the access rights of the currently impersonated User." ) @GetMapping("/list") - public Mono>> getOrgGroups(@RequestParam(required = false, defaultValue = "0") Integer pageNum, + public Mono>> getOrgGroups(@RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java index b0acc8cf1..2b2a9dd75 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationController.java @@ -50,7 +50,7 @@ public class OrganizationController implements OrganizationEndpoints @Override public Mono> getOrganizationByUser(@PathVariable String email, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize) { var flux = userService.findByEmailDeep(email).flux().flatMap(user -> orgMemberService.getAllActiveOrgs(user.getId())) .flatMap(orgMember -> organizationService.getById(orgMember.getOrgId())) diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java index 734012033..38332e892 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/usermanagement/OrganizationEndpoints.java @@ -46,7 +46,7 @@ public interface OrganizationEndpoints ) @GetMapping("/byuser/{email}") public Mono> getOrganizationByUser(@PathVariable String email, - @RequestParam(required = false, defaultValue = "0") Integer pageNum, + @RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "0") Integer pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/Pagination.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/Pagination.java index 051c3e006..03141d6bb 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/Pagination.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/util/Pagination.java @@ -64,7 +64,7 @@ public int size() { @NotNull public static Mono> fluxToPageResponseView(Integer pageNum, Integer pageSize, Flux flux) { var countMono = flux.count(); - var flux1 = flux.skip((long) pageNum * pageSize); + var flux1 = flux.skip((long) (pageNum - 1) * pageSize); if(pageSize > 0) flux1 = flux1.take(pageSize); return flux1.collectList().zipWith(countMono) .map(tuple -> PageResponseView.success(tuple.getT1(), pageNum, pageSize, Math.toIntExact(tuple.getT2()))); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java index ddf0422ab..a51a74e09 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/DatabaseChangelog.java @@ -4,6 +4,9 @@ import com.github.cloudyrock.mongock.ChangeSet; import com.github.cloudyrock.mongock.driver.mongodb.springdata.v4.decorator.impl.MongockTemplate; import com.github.f4b6a3.uuid.UuidCreator; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; +import com.mongodb.client.result.DeleteResult; import lombok.extern.slf4j.Slf4j; import org.bson.Document; import org.lowcoder.domain.application.model.Application; @@ -44,6 +47,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Arrays; import java.util.List; import java.util.Set; @@ -313,41 +317,86 @@ private int getMongoDBVersion(MongockTemplate mongoTemplate) { @ChangeSet(order = "026", id = "add-time-series-snapshot-history", author = "") public void addTimeSeriesSnapshotHistory(MongockTemplate mongoTemplate, CommonConfig commonConfig) { int mongoVersion = getMongoDBVersion(mongoTemplate); - if (mongoVersion < 5) { - log.warn("MongoDB version is below 5. Time-series collections are not supported. Upgrade the MongoDB version."); - } - // Create the time-series collection if it doesn't exist - if (!mongoTemplate.collectionExists(ApplicationHistorySnapshotTS.class)) { - if(mongoVersion < 5) { - mongoTemplate.createCollection(ApplicationHistorySnapshotTS.class); - } else { - mongoTemplate.createCollection(ApplicationHistorySnapshotTS.class, CollectionOptions.empty().timeSeries("createdAt")); + Instant thresholdDate = Instant.now().minus(commonConfig.getQuery().getAppSnapshotKeepDuration(), ChronoUnit.DAYS); + + if (mongoVersion >= 5) { + // MongoDB version >= 5: Use manual insert query + if (!mongoTemplate.collectionExists(ApplicationHistorySnapshotTS.class)) { + mongoTemplate.createCollection(ApplicationHistorySnapshotTS.class, + CollectionOptions.empty().timeSeries("createdAt")); } + + // Aggregation pipeline to fetch the records + List aggregationPipeline = Arrays.asList( + new Document("$match", new Document("createdAt", new Document("$lte", thresholdDate))), + new Document("$project", new Document() + .append("applicationId", 1) + .append("dsl", 1) + .append("context", 1) + .append("createdAt", 1) + .append("createdBy", 1) + .append("modifiedBy", 1) + .append("updatedAt", 1) + .append("id", "$_id")) // Map `_id` to `id` if needed + ); + + MongoCollection sourceCollection = mongoTemplate.getDb().getCollection("applicationHistorySnapshot"); + MongoCollection targetCollection = mongoTemplate.getDb().getCollection("applicationHistorySnapshotTS"); + + // Fetch results and insert them into the time-series collection + try (MongoCursor cursor = sourceCollection.aggregate(aggregationPipeline).iterator()) { + while (cursor.hasNext()) { + Document document = cursor.next(); + targetCollection.insertOne(document); // Insert into the time-series collection + } + } + + // Delete the migrated records + Query deleteQuery = new Query(Criteria.where("createdAt").gte(thresholdDate)); + DeleteResult deleteResult = mongoTemplate.remove(deleteQuery, ApplicationHistorySnapshot.class); + + log.info("Deleted {} records from the source collection.", deleteResult.getDeletedCount()); + } else { + // MongoDB version < 5: Use aggregation with $out + if (!mongoTemplate.collectionExists(ApplicationHistorySnapshotTS.class)) { + mongoTemplate.createCollection(ApplicationHistorySnapshotTS.class); // Create a regular collection + } + + // Aggregation pipeline with $out + List aggregationPipeline = Arrays.asList( + new Document("$match", new Document("createdAt", new Document("$lte", thresholdDate))), + new Document("$project", new Document() + .append("applicationId", 1) + .append("dsl", 1) + .append("context", 1) + .append("createdAt", 1) + .append("createdBy", 1) + .append("modifiedBy", 1) + .append("updatedAt", 1) + .append("id", "$_id")), // Map `_id` to `id` if needed + new Document("$out", "applicationHistorySnapshotTS") // Write directly to the target collection + ); + + mongoTemplate.getDb() + .getCollection("applicationHistorySnapshot") + .aggregate(aggregationPipeline) + .toCollection(); + + // Delete the migrated records + Query deleteQuery = new Query(Criteria.where("createdAt").gte(thresholdDate)); + DeleteResult deleteResult = mongoTemplate.remove(deleteQuery, ApplicationHistorySnapshot.class); + + log.info("Deleted {} records from the source collection.", deleteResult.getDeletedCount()); } - Instant thresholdDate = Instant.now().minus(commonConfig.getQuery().getAppSnapshotKeepDuration(), ChronoUnit.DAYS); - List snapshots = mongoTemplate.find(new Query().addCriteria(Criteria.where("createdAt").gte(thresholdDate)), ApplicationHistorySnapshot.class); - snapshots.forEach(snapshot -> { - ApplicationHistorySnapshotTS applicationHistorySnapshotTS = new ApplicationHistorySnapshotTS(); - applicationHistorySnapshotTS.setApplicationId(snapshot.getApplicationId()); - applicationHistorySnapshotTS.setDsl(snapshot.getDsl()); - applicationHistorySnapshotTS.setContext(snapshot.getContext()); - applicationHistorySnapshotTS.setCreatedAt(snapshot.getCreatedAt()); - applicationHistorySnapshotTS.setCreatedBy(snapshot.getCreatedBy()); - applicationHistorySnapshotTS.setModifiedBy(snapshot.getModifiedBy()); - applicationHistorySnapshotTS.setUpdatedAt(snapshot.getUpdatedAt()); - applicationHistorySnapshotTS.setId(snapshot.getId()); - mongoTemplate.insert(applicationHistorySnapshotTS); - mongoTemplate.remove(snapshot); - }); - // Ensure indexes if needed + // Ensure indexes on the new collection ensureIndexes(mongoTemplate, ApplicationHistorySnapshotTS.class, makeIndex("applicationId"), - makeIndex("createdAt") - ); + makeIndex("createdAt")); } + private void addGidField(MongockTemplate mongoTemplate, String collectionName) { // Create a query to match all documents Query query = new Query(); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/task/ArchiveSnapshotTask.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/task/ArchiveSnapshotTask.java index 2fa516379..28108f51a 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/task/ArchiveSnapshotTask.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/task/ArchiveSnapshotTask.java @@ -2,12 +2,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; -import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; import org.lowcoder.sdk.config.CommonConfig; import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -16,6 +12,11 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; +import com.mongodb.client.model.Filters; +import org.bson.Document; + @Slf4j @RequiredArgsConstructor @Component @@ -24,23 +25,122 @@ public class ArchiveSnapshotTask { private final CommonConfig commonConfig; private final MongoTemplate mongoTemplate; - @Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.DAYS) + @Scheduled(initialDelay = 0, fixedRate = 1, timeUnit = TimeUnit.DAYS) public void archive() { + int mongoVersion = getMongoDBVersion(); Instant thresholdDate = Instant.now().minus(commonConfig.getQuery().getAppSnapshotKeepDuration(), ChronoUnit.DAYS); - List snapshots = mongoTemplate.find(new Query().addCriteria(Criteria.where("createdAt").lte(thresholdDate)), ApplicationHistorySnapshotTS.class); - snapshots.forEach(snapshot -> { - ApplicationHistorySnapshot applicationHistorySnapshot = new ApplicationHistorySnapshot(); - applicationHistorySnapshot.setApplicationId(snapshot.getApplicationId()); - applicationHistorySnapshot.setDsl(snapshot.getDsl()); - applicationHistorySnapshot.setContext(snapshot.getContext()); - applicationHistorySnapshot.setCreatedAt(snapshot.getCreatedAt()); - applicationHistorySnapshot.setCreatedBy(snapshot.getCreatedBy()); - applicationHistorySnapshot.setModifiedBy(snapshot.getModifiedBy()); - applicationHistorySnapshot.setUpdatedAt(snapshot.getUpdatedAt()); - applicationHistorySnapshot.setId(snapshot.getId()); - mongoTemplate.insert(applicationHistorySnapshot); - mongoTemplate.remove(snapshot); - }); + + if (mongoVersion >= 5) { + archiveForVersion5AndAbove(thresholdDate); + } else { + archiveForVersionBelow5(thresholdDate); + } + } + + private int getMongoDBVersion() { + Document buildInfo = mongoTemplate.getDb().runCommand(new Document("buildInfo", 1)); + String version = buildInfo.getString("version"); + return Integer.parseInt(version.split("\\.")[0]); // Parse major version } + private void archiveForVersion5AndAbove(Instant thresholdDate) { + log.info("Running archival for MongoDB version >= 5"); + + MongoCollection sourceCollection = mongoTemplate.getDb().getCollection("applicationHistorySnapshot"); + MongoCollection targetCollection = mongoTemplate.getDb().getCollection("applicationHistorySnapshotTS"); + + long totalDocuments = sourceCollection.countDocuments(Filters.lte("createdAt", thresholdDate)); + log.info("Total documents to archive: {}", totalDocuments); + + long processedCount = 0; + + try (MongoCursor cursor = sourceCollection.find(Filters.lte("createdAt", thresholdDate)).iterator()) { + while (cursor.hasNext()) { + Document document = cursor.next(); + + // Transform the document for the target collection + document.put("id", document.getObjectId("_id")); // Map `_id` to `id` + document.remove("_id"); + + // Insert the document into the target collection + try { + targetCollection.insertOne(document); + } catch (Exception e) { + log.error("Failed to insert document with ID {}. Error: {}", document.getObjectId("id"), e.getMessage()); + continue; + } + + // Remove the document from the source collection + try { + sourceCollection.deleteOne(Filters.eq("_id", document.getObjectId("id"))); + } catch (Exception e) { + log.error("Failed to delete document with ID {}. Error: {}", document.getObjectId("id"), e.getMessage()); + continue; + } + + processedCount++; + log.info("Processed document {} / {}", processedCount, totalDocuments); + } + } catch (Exception e) { + log.error("Failed during archival process. Error: {}", e.getMessage()); + } + + log.info("Archival process completed. Total documents archived: {}", processedCount); + } + + private void archiveForVersionBelow5(Instant thresholdDate) { + log.info("Running archival for MongoDB version < 5"); + + MongoCollection sourceCollection = mongoTemplate.getDb().getCollection("applicationHistorySnapshot"); + + long totalDocuments = sourceCollection.countDocuments(Filters.lte("createdAt", thresholdDate)); + log.info("Total documents to archive: {}", totalDocuments); + + long processedCount = 0; + + try (MongoCursor cursor = sourceCollection.find(Filters.lte("createdAt", thresholdDate)).iterator()) { + while (cursor.hasNext()) { + Document document = cursor.next(); + + // Transform the document for the target collection + document.put("id", document.getObjectId("_id")); // Map `_id` to `id` + document.remove("_id"); + + // Use aggregation with $out for the single document + try { + sourceCollection.aggregate(List.of( + Filters.eq("_id", document.getObjectId("id")), + new Document("$project", new Document() + .append("applicationId", document.get("applicationId")) + .append("dsl", document.get("dsl")) + .append("context", document.get("context")) + .append("createdAt", document.get("createdAt")) + .append("createdBy", document.get("createdBy")) + .append("modifiedBy", document.get("modifiedBy")) + .append("updatedAt", document.get("updatedAt")) + .append("id", document.get("id"))), + new Document("$out", "applicationHistorySnapshotTS") + )).first(); + } catch (Exception e) { + log.error("Failed to aggregate and insert document with ID {}. Error: {}", document.getObjectId("id"), e.getMessage()); + continue; + } + + // Remove the document from the source collection + try { + sourceCollection.deleteOne(Filters.eq("_id", document.getObjectId("id"))); + } catch (Exception e) { + log.error("Failed to delete document with ID {}. Error: {}", document.getObjectId("id"), e.getMessage()); + continue; + } + + processedCount++; + log.info("Processed document {} / {}", processedCount, totalDocuments); + } + } catch (Exception e) { + log.error("Failed during archival process. Error: {}", e.getMessage()); + } + + log.info("Archival process completed. Total documents archived: {}", processedCount); + } } diff --git a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/impl/ApplicationHistorySnapshotServiceTest.java b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/impl/ApplicationHistorySnapshotServiceTest.java index fb7109134..81c0cb56d 100644 --- a/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/impl/ApplicationHistorySnapshotServiceTest.java +++ b/server/api-service/lowcoder-server/src/test/java/org/lowcoder/api/service/impl/ApplicationHistorySnapshotServiceTest.java @@ -4,7 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.junit.runner.RunWith; -import org.lowcoder.domain.application.model.ApplicationHistorySnapshotTS; +import org.lowcoder.domain.application.model.ApplicationHistorySnapshot; import org.lowcoder.domain.application.service.ApplicationHistorySnapshotService; import org.lowcoder.sdk.models.HasIdAndAuditing; import org.springframework.beans.factory.annotation.Autowired; @@ -47,8 +47,8 @@ public void testServiceMethods() { .assertNext(list -> { assertEquals(2, list.size()); - ApplicationHistorySnapshotTS first = list.get(0); - ApplicationHistorySnapshotTS second = list.get(1); + ApplicationHistorySnapshot first = list.get(0); + ApplicationHistorySnapshot second = list.get(1); assertTrue(first.getCreatedAt().isAfter(second.getCreatedAt())); assertNull(first.getDsl()); @@ -66,7 +66,7 @@ public void testServiceMethods() { StepVerifier.create(service.listAllHistorySnapshotBriefInfo(applicationId, null, null, null, null, PageRequest.of(1, 1))) .assertNext(list -> { assertEquals(1, list.size()); - ApplicationHistorySnapshotTS one = list.get(0); + ApplicationHistorySnapshot one = list.get(0); assertNull(one.getDsl()); assertEquals(ImmutableMap.of("context", "context1"), one.getContext()); assertEquals(applicationId, one.getApplicationId());