diff --git a/.gitignore b/.gitignore index 8758dff248..1dcb36aaa0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ application-dev-localhost.yaml server/api-service/lowcoder-server/src/main/resources/application-local-dev.yaml translations/locales/node_modules/ .vscode/settings.json +server/api-service/lowcoder-server/src/main/resources/application-local-dev-ee.yaml diff --git a/client/packages/lowcoder-design/src/components/Search.tsx b/client/packages/lowcoder-design/src/components/Search.tsx index 11e5f2adcb..dff0ebeeb9 100644 --- a/client/packages/lowcoder-design/src/components/Search.tsx +++ b/client/packages/lowcoder-design/src/components/Search.tsx @@ -62,24 +62,35 @@ interface ISearch { placeholder: string; value: string; onChange: (value: React.ChangeEvent) => void; + onEnterPress?: (value: string) => void; // Added for capturing Enter key press disabled?: boolean; } export const Search = (props: ISearch & InputProps) => { - const { value, onChange, style, disabled, placeholder, ...others } = props; + const { value, onChange, style, disabled, placeholder, onEnterPress, ...others } = props; + const handleChange = (e: React.ChangeEvent) => { onChange && onChange(e); }; + + // Handling Enter key press + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && onEnterPress) { + onEnterPress(value); + } + }; + return ( - - } - {...others} - /> - + + } + {...others} + /> + ); -}; +}; \ No newline at end of file diff --git a/client/packages/lowcoder/index.html b/client/packages/lowcoder/index.html index f3019a0cd4..b9f940e010 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/api/applicationApi.ts b/client/packages/lowcoder/src/api/applicationApi.ts index a0edb74243..2411b50d80 100644 --- a/client/packages/lowcoder/src/api/applicationApi.ts +++ b/client/packages/lowcoder/src/api/applicationApi.ts @@ -12,7 +12,7 @@ import { SetAppEditingStatePayload, UpdateAppPermissionPayload, } from "redux/reduxActions/applicationActions"; -import { ApiResponse, GenericApiResponse } from "./apiResponses"; +import {ApiResponse, GenericApiResponse} from "./apiResponses"; import { JSONObject, JSONValue } from "util/jsonTypes"; import { ApplicationDetail, @@ -24,6 +24,7 @@ import { } from "constants/applicationConstants"; import { CommonSettingResponseData } from "./commonSettingApi"; import { ResourceType } from "@lowcoder-ee/constants/queryConstants"; +import {fetchAppRequestType, GenericApiPaginationResponse} from "@lowcoder-ee/util/pagination/type"; export interface HomeOrgMeta { id: string; @@ -108,6 +109,10 @@ class ApplicationApi extends Api { return Api.get(ApplicationApi.newURLPrefix + "/list", { ...request, withContainerSize: false }); } + static fetchAllApplicationsPagination(request: fetchAppRequestType): AxiosPromise> { + return Api.get(ApplicationApi.newURLPrefix + "/list", { ...request, withContainerSize: false, applicationStatus: "RECYCLED" }); + } + static fetchAllModules(request: HomeDataPayload): AxiosPromise { return Api.get(ApplicationApi.newURLPrefix + "/list", { applicationType: AppTypeEnum.Module, diff --git a/client/packages/lowcoder/src/api/datasourceApi.ts b/client/packages/lowcoder/src/api/datasourceApi.ts index ea08bb9346..d5973c0e93 100644 --- a/client/packages/lowcoder/src/api/datasourceApi.ts +++ b/client/packages/lowcoder/src/api/datasourceApi.ts @@ -8,6 +8,7 @@ import { JSONArray } from "util/jsonTypes"; import { AuthType, HttpOAuthGrantType } from "pages/datasource/form/httpDatasourceForm"; import { Datasource } from "@lowcoder-ee/constants/datasourceConstants"; import { DataSourcePluginMeta } from "lowcoder-sdk/dataSource"; +import {fetchDBRequestType, GenericApiPaginationResponse} from "@lowcoder-ee/util/pagination/type"; export interface PreparedStatementConfig { enableTurnOffPreparedStatement: boolean; @@ -172,6 +173,11 @@ export class DatasourceApi extends Api { return Api.get(DatasourceApi.url + `/listByOrg?orgId=${orgId}`); } + static fetchDatasourcePaginationByOrg(request: fetchDBRequestType): AxiosPromise> { + const {orgId, ...res} = request; + return Api.get(DatasourceApi.url + `/listByOrg?orgId=${orgId}`, {...res}); + } + static createDatasource( datasourceConfig: Partial ): AxiosPromise> { diff --git a/client/packages/lowcoder/src/api/folderApi.ts b/client/packages/lowcoder/src/api/folderApi.ts index 0f2fd47e59..30aef9251b 100644 --- a/client/packages/lowcoder/src/api/folderApi.ts +++ b/client/packages/lowcoder/src/api/folderApi.ts @@ -9,6 +9,10 @@ import { UpdateFolderPayload, } from "../redux/reduxActions/folderActions"; import { ApplicationMeta, FolderMeta } from "../constants/applicationConstants"; +import { + fetchFolderRequestType, + GenericApiPaginationResponse +} from "@lowcoder-ee/util/pagination/type"; export class FolderApi extends Api { static url = "/folders"; @@ -40,4 +44,10 @@ export class FolderApi extends Api { ): AxiosPromise> { return Api.get(FolderApi.url + `/elements`, { id: request.folderId }); } + + static fetchFolderElementsPagination( + request: fetchFolderRequestType + ): AxiosPromise> { + return Api.get(FolderApi.url + `/elements`, { ...request }); + } } diff --git a/client/packages/lowcoder/src/api/orgApi.ts b/client/packages/lowcoder/src/api/orgApi.ts index 6e7c532e4d..5e650417db 100644 --- a/client/packages/lowcoder/src/api/orgApi.ts +++ b/client/packages/lowcoder/src/api/orgApi.ts @@ -10,6 +10,13 @@ import { UpdateUserOrgRolePayload, } from "redux/reduxActions/orgActions"; import { ApiResponse, GenericApiResponse } from "./apiResponses"; +import { + fetchGroupUserRequestType, + fetchOrgUserRequestType, + GenericApiPaginationResponse, + GroupUsersPaginationResponse, + orgGroupRequestType, OrgUsersPaginationResponse +} from "@lowcoder-ee/util/pagination/type"; export interface GroupUsersResponse extends ApiResponse { data: { @@ -66,6 +73,10 @@ export class OrgApi extends Api { return Api.get(OrgApi.fetchGroupURL); } + static fetchGroupPagination(request: orgGroupRequestType): AxiosPromise> { + return Api.get(OrgApi.fetchGroupURL, {...request}); + } + static deleteGroup(groupId: string): AxiosPromise { return Api.delete(OrgApi.deleteGroupURL(groupId)); } @@ -88,10 +99,20 @@ export class OrgApi extends Api { return Api.get(OrgApi.fetchOrgUsersURL(orgId)); } + static fetchOrgUsersPagination(request:fetchOrgUserRequestType): AxiosPromise { + const {orgId, ...res} = request; + return Api.get(OrgApi.fetchOrgUsersURL(orgId), {...res}); + } + static fetchGroupUsers(groupId: string): AxiosPromise { return Api.get(OrgApi.fetchGroupUsersURL(groupId)); } + static fetchGroupUsersPagination(request: fetchGroupUserRequestType): AxiosPromise { + const {groupId, ...res} = request; + return Api.get(OrgApi.fetchGroupUsersURL(groupId), {...res}); + } + static deleteGroupUser(request: RemoveGroupUserPayload): AxiosPromise { return Api.delete(OrgApi.deleteGroupUserURL(request.groupId), { userId: request.userId, diff --git a/client/packages/lowcoder/src/components/DraggableTree/DraggableItem.tsx b/client/packages/lowcoder/src/components/DraggableTree/DraggableItem.tsx index c8a0f093e7..4d827381c8 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 68c355ec37..7c9eac729f 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 5fede0b077..83fe577c90 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 be3cd62718..6df5991f2d 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 a4672903eb..999890d016 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 086d19d0e0..2072fc8495 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/BackButton.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/BackButton.tsx new file mode 100644 index 0000000000..65952d07cc --- /dev/null +++ b/client/packages/lowcoder/src/pages/ApplicationV2/BackButton.tsx @@ -0,0 +1,4 @@ +export const BackButton = () =>{ + return +
123
+} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx index e69792bbd5..c9d8dee642 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeLayout.tsx @@ -35,6 +35,7 @@ import { isFetchingFolderElements } from "../../redux/selectors/folderSelector"; import { checkIsMobile } from "util/commonUtils"; import { default as Divider } from "antd/es/divider"; import { ApplicationCategoriesEnum } from "constants/applicationConstants"; +import { Pagination } from 'antd'; const Wrapper = styled.div` display: flex; @@ -199,6 +200,12 @@ const EmptyView = styled.div` } } `; +const PaginationLayout = styled.div` + display: flex; + justify-content: center; + margin-top: -20px; + margin-bottom: 20px; +` const LayoutSwitcher = styled.div` position: absolute; @@ -301,11 +308,39 @@ export interface HomeLayoutProps { localMarketplaceApps?: Array; globalMarketplaceApps?: Array; mode: HomeLayoutMode; + setCurrentPage?: any; + setPageSize?: any; + currentPage?: number; + pageSize?: number; + total?: number; + searchValues?: number; + setSearchValues?: any; + setTypeFilterPagination?: any; } export function HomeLayout(props: HomeLayoutProps) { + const { breadcrumb = [], + elements = [], + localMarketplaceApps = [], + globalMarketplaceApps = [], + mode , + setCurrentPage, + setPageSize, + pageSize, + currentPage, + searchValues, + setSearchValues, + total, + setTypeFilterPagination, + + } = props; + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; - const { breadcrumb = [], elements = [], localMarketplaceApps = [], globalMarketplaceApps = [], mode } = props; + const handlePageSizeChange = (current: number, size: number) => { + setPageSize(size); + }; const categoryOptions = [ { label: {trans("home.allCategories")}, value: 'All' }, @@ -325,6 +360,7 @@ export function HomeLayout(props: HomeLayoutProps) { const [typeFilter, setTypeFilter] = useState("All"); const [categoryFilter, setCategoryFilter] = useState("All"); const [searchValue, setSearchValue] = useState(""); + const [visibility, setVisibility] = useState(mode === "view" || mode === "trash"); const [layout, setLayout] = useState( checkIsMobile(window.innerWidth) ? "card" : getHomeLayout() ); @@ -342,7 +378,15 @@ export function HomeLayout(props: HomeLayoutProps) { return null; } - var displayElements = elements; + var displayElements = elements.sort((a, b) => { + if (a.folder && !b.folder) { + return -1; + } else if (!a.folder && b.folder) { + return 1; + } else { + return 0; + } + }); if (mode === "marketplace" && isSelfHost) { const markedLocalApps = localMarketplaceApps.map(app => ({ ...app, isLocalMarketplace: true })); @@ -354,27 +398,34 @@ export function HomeLayout(props: HomeLayoutProps) { const markedLocalApps = localMarketplaceApps.map(app => ({ ...app, isLocalMarketplace: true })); displayElements = [...markedLocalApps]; } - const resList: HomeRes[] = displayElements - .filter((e) => - searchValue - ? e.name?.toLocaleLowerCase().includes(searchValue?.toLocaleLowerCase()) || - e.createBy?.toLocaleLowerCase().includes(searchValue?.toLocaleLowerCase()) - : true - ) .filter((e) => { - if (HomeResTypeEnum[typeFilter].valueOf() === HomeResTypeEnum.All) { + if (!visibility) { + if (searchValue) { + const lowerCaseSearchValue = searchValue.toLocaleLowerCase(); + return e.name?.toLocaleLowerCase().includes(lowerCaseSearchValue) || + e.createBy?.toLocaleLowerCase().includes(lowerCaseSearchValue); + } return true; } - if (e.folder) { - return HomeResTypeEnum[typeFilter] === HomeResTypeEnum.Folder; - } else { - if (typeFilter === "Navigation") { - return NavigationTypes.map((t) => t.valueOf()).includes(e.applicationType); + return true; + }) + .filter((e) => { + if(!visibility) { + if (HomeResTypeEnum[typeFilter].valueOf() === HomeResTypeEnum.All) { + return true; + } + if (e.folder) { + return HomeResTypeEnum[typeFilter] === HomeResTypeEnum.Folder; + } else { + if (typeFilter === "Navigation") { + return NavigationTypes.map((t) => t.valueOf()).includes(e.applicationType); + } + return HomeResTypeEnum[typeFilter].valueOf() === e.applicationType; } - return HomeResTypeEnum[typeFilter].valueOf() === e.applicationType; } - }) + return true; + }) .filter((e) => { // If "All" is selected, do not filter out any elements based on category if (categoryFilter === 'All' || !categoryFilter) { @@ -415,6 +466,7 @@ export function HomeLayout(props: HomeLayoutProps) { } ); + const getFilterMenuItem = (type: HomeResTypeEnum) => { const Icon = HomeResInfo[type].icon; return { @@ -462,7 +514,7 @@ export function HomeLayout(props: HomeLayoutProps) { {showNewUserGuide(user) && } - +

{mode === "marketplace" && trans("home.appMarketplace")} @@ -480,7 +532,12 @@ export function HomeLayout(props: HomeLayoutProps) { setTypeFilter(value as HomeResKey)} + onChange={(value: any) => { + setTypeFilter(value as HomeResKey); + if(visibility) + setTypeFilterPagination(HomeResTypeEnum[value]) + } + } options={[ getFilterMenuItem(HomeResTypeEnum.All), getFilterMenuItem(HomeResTypeEnum.Application), @@ -507,6 +564,7 @@ export function HomeLayout(props: HomeLayoutProps) { placeholder={trans("search")} value={searchValue} onChange={(e) => setSearchValue(e.target.value)} + onEnterPress={(value) => setSearchValues(value)} style={{ width: "192px", height: "32px", margin: "0" }} /> {mode !== "trash" && mode !== "marketplace" && user.orgDev && ( @@ -603,10 +661,21 @@ export function HomeLayout(props: HomeLayoutProps) { )} - + {visibility && resList.length ?
+ + + +
: null} - + ); diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeView.tsx index b4309e321e..f32ec84530 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeView.tsx @@ -1,12 +1,48 @@ import { useSelector } from "react-redux"; import { HomeLayout } from "./HomeLayout"; import { getUser } from "../../redux/selectors/usersSelectors"; -import { folderElementsSelector } from "../../redux/selectors/folderSelector"; import { Helmet } from "react-helmet"; import { trans } from "i18n"; +import {useState, useEffect } from "react"; +import {fetchFolderElements} from "@lowcoder-ee/util/pagination/axios"; +import {ApplicationMeta, FolderMeta} from "@lowcoder-ee/constants/applicationConstants"; +import {ApplicationPaginationType} from "@lowcoder-ee/util/pagination/type"; + +interface ElementsState { + elements: (ApplicationMeta | FolderMeta)[]; + total: number; +} export function HomeView() { - const elements = useSelector(folderElementsSelector)[""]; + const [elements, setElements] = useState({ elements: [], total: 1 }); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [searchValues, setSearchValues] = useState(""); + const [typeFilter, setTypeFilter] = useState(0); + useEffect( () => { + try{ + fetchFolderElements({ + pageNum:currentPage, + pageSize:pageSize, + applicationType: ApplicationPaginationType[typeFilter], + name: searchValues, + }).then( + data => { + if (data.success) { + setElements({elements: data.data || [], total: data.total || 1}) + } + else + console.error("ERROR: fetchFolderElements", data.error) + } + ); + } catch (error) { + console.error('Failed to fetch data:', error); + } + }, [currentPage, pageSize, searchValues, typeFilter] + ); + + console.log(currentPage, pageSize); + const user = useSelector(getUser); if (!user.currentOrgId) { @@ -16,9 +52,16 @@ export function HomeView() { return ( <> {{trans("productName")} {trans("home.home")}} - ); diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/RootFolderListView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/RootFolderListView.tsx deleted file mode 100644 index a2263017cf..0000000000 --- a/client/packages/lowcoder/src/pages/ApplicationV2/RootFolderListView.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useSelector } from "react-redux"; -import { HomeLayout } from "./HomeLayout"; -import { getUser } from "../../redux/selectors/usersSelectors"; -import { FOLDERS_URL } from "../../constants/routesURL"; -import { trans } from "../../i18n"; -import { foldersSelector } from "../../redux/selectors/folderSelector"; - -export function RootFolderListView() { - const user = useSelector(getUser); - const allFolders = useSelector(foldersSelector); - - if (!user.currentOrgId) { - return null; - } - - return ( - - ); -} diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/TrashView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/TrashView.tsx index d1b0586c27..a273f0cb34 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/TrashView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/TrashView.tsx @@ -1,27 +1,61 @@ import { HomeLayout } from "./HomeLayout"; -import { useDispatch, useSelector } from "react-redux"; -import { recycleListSelector } from "../../redux/selectors/applicationSelector"; import { TRASH_URL } from "../../constants/routesURL"; -import { useEffect } from "react"; -import { fetchApplicationRecycleList } from "../../redux/reduxActions/applicationActions"; +import {useEffect, useState} from "react"; import { trans } from "../../i18n"; import { Helmet } from "react-helmet"; +import {fetchApplicationElements} from "@lowcoder-ee/util/pagination/axios"; + +interface ElementsState { + elements: any; + total: number; +} export function TrashView() { - const dispatch = useDispatch(); - const recycleList = useSelector(recycleListSelector); + const [elements, setElements] = useState({ elements: [], total: 1 }); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [searchValues, setSearchValues] = useState(""); + const [typeFilter, setTypeFilter] = useState(0); - useEffect(() => { - dispatch(fetchApplicationRecycleList()); - }, [dispatch]); + useEffect( () => { + if (typeFilter === 7) // Application of Navigation is 3 in API. + setTypeFilter(3); + try{ + fetchApplicationElements({ + pageNum:currentPage, + pageSize:pageSize, + applicationType: typeFilter, + name: searchValues, + }).then( + data => { + if (data.success) { + setElements({elements: data.data || [], total: data.total || 1}) + } + else + console.error("ERROR: fetchFolderElements", data.error) + } + ); + } catch (error) { + console.error('Failed to fetch data:', error); + } + }, [currentPage, pageSize, searchValues, typeFilter] + ); return ( <> {{trans("home.trash")}} + elements={elements.elements} + breadcrumb={[{ text: trans("home.trash"), path: TRASH_URL }]} + mode={"trash"} + currentPage ={currentPage} + setCurrentPage={setCurrentPage} + pageSize={pageSize} + setPageSize={setPageSize} + total={elements.total} + setSearchValues={setSearchValues} + setTypeFilterPagination={setTypeFilter} + /> ); } diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx index c6fd5f91fb..5a3a2f3fac 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx @@ -4,7 +4,6 @@ import { DATASOURCE_URL, FOLDER_URL, FOLDER_URL_PREFIX, - FOLDERS_URL, MARKETPLACE_URL, QUERY_LIBRARY_URL, SETTING_URL, @@ -53,7 +52,6 @@ import { FolderView } from "./FolderView"; import { TrashView } from "./TrashView"; import { MarketplaceView } from "./MarketplaceView"; // import { SideBarItemType } from "../../components/layout/SideBarSection"; -import { RootFolderListView } from "./RootFolderListView"; // import InviteDialog from "../common/inviteDialog"; import { fetchFolderElements, updateFolder } from "../../redux/reduxActions/folderActions"; // import { ModuleView } from "./ModuleView"; @@ -73,7 +71,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 +220,7 @@ export default function ApplicationHome() { return ( + {trans("home.allFolders")}, - routePath: FOLDERS_URL, - routeComp: RootFolderListView, - icon: ({ selected, ...otherProps }) => selected ? : , - }, + // { + // text: {trans("home.allFolders")}, + // routePath: FOLDERS_URL, + // routeComp: RootFolderListView, + // icon: ({ selected, ...otherProps }) => selected ? : , + // }, { text: {trans("home.allApplications")}, routePath: ALL_APPLICATIONS_URL, diff --git a/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx b/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx index 87fb7ec08e..01c0861633 100644 --- a/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx +++ b/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx @@ -1,6 +1,6 @@ import styled from "styled-components"; import { EditPopover, PointIcon, Search, TacoButton } from "lowcoder-design"; -import React, { useState } from "react"; +import React, {useEffect, useState} from "react"; import { useDispatch, useSelector } from "react-redux"; import { getDataSource, getDataSourceLoading, getDataSourceTypesMap } from "../../redux/selectors/datasourceSelectors"; import { deleteDatasource } from "../../redux/reduxActions/datasourceActions"; @@ -17,6 +17,10 @@ import { DatasourcePermissionDialog } from "../../components/PermissionDialog/Da import DataSourceIcon from "components/DataSourceIcon"; import { Helmet } from "react-helmet"; import LoadingOutlined from "@ant-design/icons/LoadingOutlined"; +import PaginationComp from "@lowcoder-ee/util/pagination/Pagination"; +import {DatasourceInfo} from "@lowcoder-ee/api/datasourceApi"; +import {fetchDatasourcePagination} from "@lowcoder-ee/util/pagination/axios"; +import {getUser} from "@lowcoder-ee/redux/selectors/usersSelectors"; const DatasourceWrapper = styled.div` display: flex; @@ -103,11 +107,41 @@ const StyledTable = styled(Table)` export const DatasourceList = () => { const dispatch = useDispatch(); const [searchValue, setSearchValue] = useState(""); + const [searchValues, setSearchValues] = useState(""); const [isCreateFormShow, showCreateForm] = useState(false); const [shareDatasourceId, setShareDatasourceId] = useState(undefined); const datasource = useSelector(getDataSource); + const currentUser = useSelector(getUser); + const orgId = currentUser.currentOrgId; const datasourceLoading = useSelector(getDataSourceLoading); const plugins = useSelector(getDataSourceTypesMap); + interface ElementsState { + elements: DatasourceInfo[]; + total: number; + } + console.log(datasource); + + const [elements, setElements] = useState({ elements: [], total: 0 }); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + useEffect( () => { + fetchDatasourcePagination( + { + orgId: orgId, + pageNum: currentPage, + pageSize: pageSize, + name: searchValues + } + ).then(result => { + if (result.success){ + setElements({elements: result.data || [], total: result.total || 1}) + } + else + console.error("ERROR: fetchFolderElements", result.error) + }) + }, [currentPage, pageSize, searchValues] + ) return ( <> @@ -140,6 +174,7 @@ export const DatasourceList = () => { placeholder={trans("search")} value={searchValue} onChange={(e) => setSearchValue(e.target.value)} + onEnterPress={(value) => setSearchValues(value)} style={{ width: "192px", height: "32px", margin: "0 12px 0 0" }} /> showCreateForm(true)}> {trans("home.newDatasource")} @@ -267,19 +302,7 @@ export const DatasourceList = () => { ), }, ]} - dataSource={datasource - .filter((info) => { - if (info.datasource.creationSource === 2) { - return false; - } - if (!isEmpty(searchValue)) { - return ( - info.datasource.name.toLowerCase().includes(searchValue.trim().toLowerCase()) || - info.datasource.type.toLowerCase().includes(searchValue.trim().toLowerCase()) - ); - } - return true; - }) + dataSource={elements.elements .map((info, i) => ({ key: i, id: info.datasource.id, @@ -296,6 +319,13 @@ export const DatasourceList = () => { creator: info.creatorName, edit: info.edit, }))} /> + {shareDatasourceId && ( { !visible && setShareDatasourceId(undefined); } } /> )} - + + + ); }; diff --git a/client/packages/lowcoder/src/pages/editor/bottom/BottomSidebar.tsx b/client/packages/lowcoder/src/pages/editor/bottom/BottomSidebar.tsx index 1e75ec1416..03ff67c754 100644 --- a/client/packages/lowcoder/src/pages/editor/bottom/BottomSidebar.tsx +++ b/client/packages/lowcoder/src/pages/editor/bottom/BottomSidebar.tsx @@ -323,7 +323,7 @@ const HighlightBorder = styled.div<{ $active: boolean; $foldable: boolean; $leve max-width: 100%; flex: 1; display: flex; - padding-left: ${(props) => props.$level * 20 + (props.$foldable ? 0 : 14)}px; + padding-left: ${(props) => props.$level * 10 + (props.$foldable ? 0 : 14)}px; border-radius: 4px; border: 1px solid ${(props) => (props.$active ? BorderActiveColor : "transparent")}; align-items: center; diff --git a/client/packages/lowcoder/src/pages/editor/right/ModulePanel.tsx b/client/packages/lowcoder/src/pages/editor/right/ModulePanel.tsx index dc4ad3cc91..a24b787d2a 100644 --- a/client/packages/lowcoder/src/pages/editor/right/ModulePanel.tsx +++ b/client/packages/lowcoder/src/pages/editor/right/ModulePanel.tsx @@ -1,89 +1,234 @@ -import CreateAppButton from "components/CreateAppButton"; -import { EmptyContent } from "components/EmptyContent"; -import { ApplicationMeta, AppTypeEnum } from "constants/applicationConstants"; -import { APPLICATION_VIEW_URL } from "constants/routesURL"; +import { ApplicationMeta, AppTypeEnum, FolderMeta } from "constants/applicationConstants"; import { - ActiveTextColor, - BorderActiveShadowColor, - BorderColor, - GreyTextColor, + BorderActiveColor, + NormalMenuIconColor, } from "constants/style"; -import { ModuleDocIcon } from "lowcoder-design"; -import { trans } from "i18n"; +import { APPLICATION_VIEW_URL } from "constants/routesURL"; +import { RightContext } from "./rightContext"; +import { + EditPopover, + EditText, + FoldedIcon, + ModuleDocIcon, + PointIcon, + PopupCard, + UnfoldIcon, + FileFolderIcon, messageInstance, CustomModal +} from "lowcoder-design"; +import {trans, transToNode} from "i18n"; import { draggingUtils } from "layout/draggingUtils"; -import { useContext, useEffect } from "react"; +import React, { useContext, useEffect, useState} from "react"; import { useDispatch, useSelector } from "react-redux"; -import { fetchAllModules } from "redux/reduxActions/applicationActions"; +import {fetchAllModules, recycleApplication, updateAppMetaAction} from "redux/reduxActions/applicationActions"; import styled from "styled-components"; +import CreateAppButton from "components/CreateAppButton"; import { TransparentImg } from "util/commonUtils"; -import { ExternalEditorContext } from "util/context/ExternalEditorContext"; -import { formatTimestamp } from "util/dateTimeUtils"; -import { RightContext } from "./rightContext"; -import { modulesSelector } from "../../../redux/selectors/applicationSelector"; -import { ComListTitle, ExtensionContentWrapper } from "./styledComponent"; - +import { ComListTitle } from "./styledComponent"; +import {folderElementsSelector} from "@lowcoder-ee/redux/selectors/folderSelector"; +import {DraggableTree} from "@lowcoder-ee/components/DraggableTree/DraggableTree"; +import { showAppSnapshotSelector} from "@lowcoder-ee/redux/selectors/appSnapshotSelector"; +import {DraggableTreeNode, DraggableTreeNodeItemRenderProps} from "@lowcoder-ee/components/DraggableTree/types"; +import { EmptyContent } from "components/EmptyContent"; +import {deleteFolder, moveToFolder, updateFolder} from "@lowcoder-ee/redux/reduxActions/folderActions"; const ItemWrapper = styled.div` + display: flex; + flex-direction: row; + &:last-child { + margin-bottom: 0; + } + .module-container { + display: flex; + width: 195px; + } + .module-icon { + margin-right: 4px; + width:19px; + height: 19px; + } + .module-content { + flex: 1; display: flex; - flex-direction: row; - margin-bottom: 12px; - &:last-child { - margin-bottom: 0; + flex-direction: column; + justify-content: space-around; + overflow: hidden; + } + .module-name { + //flex-grow: 1; + //margin-right: 8px; + line-height: 1.5; + font-size: 13px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } +`; + +type NodeType = { + name: string; + id: string; + isFolder: boolean; + containerSize?: { height: number; width: number }; + module?: ApplicationMeta; + children: NodeType[]; + rename: (val: string) => 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; + $level: number; } function ModuleItem(props: ModuleItemProps) { const compType = "module"; - const { meta } = props; + const { + meta , + isOverlay, + selectedID, + setSelectedID, + selectedType, + setSelectedType, + resComp, + id, + $level, + } = 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 +244,503 @@ 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 * 10 + (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/setting/permission/groupUsersPermission.tsx b/client/packages/lowcoder/src/pages/setting/permission/groupUsersPermission.tsx index c0f7c79d8e..02c4a3c905 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/groupUsersPermission.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/groupUsersPermission.tsx @@ -4,15 +4,13 @@ import { AddIcon, ArrowIcon, CustomSelect, PackUpIcon, SuperUserIcon } from "low import { trans } from "i18n"; import ProfileImage from "pages/common/profileImage"; import React, { useEffect, useMemo } from "react"; -import { connect, useDispatch } from "react-redux"; -import { AppState } from "redux/reducers"; +import { useDispatch } from "react-redux"; import { deleteGroupUserAction, fetchGroupUsersAction, quitGroupAction, updateUserGroupRoleAction, } from "redux/reduxActions/orgActions"; -import { getUser } from "redux/selectors/usersSelectors"; import styled from "styled-components"; import { formatTimestamp } from "util/dateTimeUtils"; import { currentOrgAdmin, isGroupAdmin } from "util/permissionUtils"; @@ -208,13 +206,4 @@ function GroupUsersPermission(props: GroupPermissionProp) { ); } -const mapStateToProps = (state: AppState) => { - return { - groupUsers: state.ui.org.groupUsers, - groupUsersFetching: state.ui.org.groupUsersFetching, - currentUser: getUser(state), - currentUserGroupRole: state.ui.org.currentUserGroupRole, - }; -}; - -export default connect(mapStateToProps)(GroupUsersPermission); +export default GroupUsersPermission; diff --git a/client/packages/lowcoder/src/pages/setting/permission/orgUsersPermission.tsx b/client/packages/lowcoder/src/pages/setting/permission/orgUsersPermission.tsx index 992e7e0f86..108a3521f2 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/orgUsersPermission.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/orgUsersPermission.tsx @@ -299,12 +299,4 @@ function OrgUsersPermission(props: UsersPermissionProp) { ); } -const mapStateToProps = (state: AppState) => { - return { - orgUsersFetching: state.ui.org.orgUsersFetching, - orgUsers: state.ui.org.orgUsers, - currentUser: getUser(state), - }; -}; - -export default connect(mapStateToProps)(OrgUsersPermission); +export default OrgUsersPermission; diff --git a/client/packages/lowcoder/src/pages/setting/permission/permissionDetail.tsx b/client/packages/lowcoder/src/pages/setting/permission/permissionDetail.tsx index 2e190121ef..7b5bed5e84 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/permissionDetail.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/permissionDetail.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import React, {useEffect, useState} from "react"; import { useDispatch, useSelector } from "react-redux"; import { fetchGroupsAction } from "redux/reduxActions/orgActions"; import { getUser } from "redux/selectors/usersSelectors"; @@ -6,7 +6,10 @@ import styled from "styled-components"; import GroupPermission from "./groupUsersPermission"; import UsersPermission from "./orgUsersPermission"; import { getOrgGroups } from "redux/selectors/orgSelectors"; -import { useParams } from "react-router"; +import { useParams } from "react-router-dom"; +import { AppState } from "redux/reducers"; +import {fetchGroupUsrPagination, fetchOrgUsrPagination} from "@lowcoder-ee/util/pagination/axios"; +import PaginationComp from "@lowcoder-ee/util/pagination/Pagination"; const PermissionContent = styled.div` display: flex; @@ -20,10 +23,21 @@ const PermissionContent = styled.div` const All_Users = "users"; -export default function PermissionSetting() { - const user = useSelector(getUser); +export default function PermissionSetting() { const user = useSelector(getUser); + + const [elements, setElements] = useState({ elements: [], total: 0 }); + const [orgMemberElements, setOrgMemberElements] = useState({ elements: [], total: 0 }) + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const orgId = user.currentOrgId; const orgGroups = useSelector(getOrgGroups); + const groupUsersFetching = useSelector((state: AppState) => state.ui.org.groupUsersFetching); + const currentUserGroupRole = useSelector((state: AppState) => state.ui.org.currentUserGroupRole); + const currentUser = useSelector(getUser); + const orgUsersFetching = useSelector((state: AppState) => state.ui.org.orgUsersFetching); + const orgUsers = useSelector((state: AppState) => state.ui.org.orgUsers); + const groupIdMap = new Map(orgGroups.map((group) => [group.groupId, group])); const dispatch = useDispatch(); const selectKey = useParams<{ groupId: string }>().groupId; @@ -33,19 +47,76 @@ export default function PermissionSetting() { } dispatch(fetchGroupsAction(orgId)); }, [orgId]); + + useEffect( () => { + if (selectKey !== "users") + fetchGroupUsrPagination( + { + groupId: groupIdMap.get(selectKey)!.groupId, + pageNum: currentPage, + pageSize: pageSize, + } + ).then(result => { + if (result.success){ + setElements({elements: result.data || [], total: result.total || 1}) + } + else + console.error("ERROR: fetchFolderElements", result.error) + } + ) + else + fetchOrgUsrPagination( + { + orgId: orgId, + pageNum: currentPage, + pageSize: pageSize, + } + ).then(result => { + if (result.success){ + setOrgMemberElements({elements: result.data || [], total: result.total || 1}) + } + else + console.error("ERROR: fetchFolderElements", result.error) + } + ) + }, + [currentPage, pageSize] + ) + if (!orgId) { return null; } + console.log(orgMemberElements.total, elements); return ( - - {selectKey === All_Users ? ( - - ) : ( - groupIdMap.has(selectKey) && ( - - ) - )} - + + {selectKey === All_Users ? ( + <> + + + + ) : ( + groupIdMap.has(selectKey) && ( + <> + + + + + ) + )} + ); -} +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/permission/permissionList.tsx b/client/packages/lowcoder/src/pages/setting/permission/permissionList.tsx index c72579a365..d8fc7759ff 100644 --- a/client/packages/lowcoder/src/pages/setting/permission/permissionList.tsx +++ b/client/packages/lowcoder/src/pages/setting/permission/permissionList.tsx @@ -21,7 +21,6 @@ import { } from "lowcoder-design"; import styled from "styled-components"; import { trans } from "i18n"; -import { getOrgGroups } from "redux/selectors/orgSelectors"; import { Table } from "components/Table"; import history from "util/history"; import { Level1SettingPageContentWithList, Level1SettingPageTitleWithBtn } from "../styled"; @@ -32,6 +31,8 @@ import { OrgGroup } from "constants/orgConstants"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import InviteDialog from "pages/common/inviteDialog"; import { Flex } from "antd"; +import {fetchOrgGroups} from "@lowcoder-ee/util/pagination/axios"; +import PaginationComp from "@lowcoder-ee/util/pagination/Pagination"; const NEW_GROUP_PREFIX = trans("memberSettings.newGroupPrefix"); @@ -51,17 +52,48 @@ type DataItemInfo = { group?: OrgGroup; }; +interface ElementsState { + elements: OrgGroup[]; + total: number; +} + export default function PermissionSetting() { + let dataSource: DataItemInfo[] = []; const user = useSelector(getUser); const orgId = user.currentOrgId; - const orgGroups = useSelector(getOrgGroups); - const visibleOrgGroups = orgGroups.filter((g) => !g.allUsersGroup); - const allUsersGroup = orgGroups.find((g) => g.allUsersGroup); const dispatch = useDispatch(); const [needRenameId, setNeedRenameId] = useState(undefined); const { nameSuffixFunc, menuItemsFunc, menuExtraView } = usePermissionMenuItems(orgId); const [groupCreating, setGroupCreating] = useState(false); + const [elements, setElements] = useState({ elements: [], total: 0 }); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + useEffect( () => { + fetchOrgGroups( + { + pageNum: currentPage, + pageSize: pageSize, + } + ).then(result => { + if (result.success){ + setElements({elements: result.data || [], total: result.total || 1}) + } + else + console.error("ERROR: fetchFolderElements", result.error) + }) + }, [currentPage, pageSize] + ) + const visibleOrgGroups = elements.elements.filter((g) => !g.allUsersGroup); + const allUsersGroup = elements.elements.find((g) => g.allUsersGroup); + dataSource = currentPage === 1 ? [{ + key: "users", + label: trans("memberSettings.allMembers"), + createTime: allUsersGroup?.createTime, + lock: true, + del: false, + rename: false, + }] : []; useEffect(() => { if (!orgId) { return; @@ -105,17 +137,6 @@ export default function PermissionSetting() { }); }; - const dataSource: DataItemInfo[] = [ - { - key: "users", - label: trans("memberSettings.allMembers"), - createTime: allUsersGroup?.createTime, - lock: true, - del: false, - rename: false, - }, - ]; - visibleOrgGroups.forEach((group) => { dataSource.push({ key: group.groupId, @@ -255,6 +276,13 @@ export default function PermissionSetting() { />
{menuExtraView} + ); } diff --git a/client/packages/lowcoder/src/pages/userAuth/index.tsx b/client/packages/lowcoder/src/pages/userAuth/index.tsx index 40e7a1bc15..d33b48fdec 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 c27cb8d507..4326cb2ecf 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 ba288b89a4..5c00aafe60 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 65a39f030d..62b74659e8 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 0000000000..f4c12c345b --- /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/client/packages/lowcoder/src/util/pagination/Pagination.tsx b/client/packages/lowcoder/src/util/pagination/Pagination.tsx new file mode 100644 index 0000000000..fe93dfc935 --- /dev/null +++ b/client/packages/lowcoder/src/util/pagination/Pagination.tsx @@ -0,0 +1,52 @@ +import styled from "styled-components"; +import { Pagination } from "antd"; + +const PaginationLayout = styled(Pagination)` + display: flex; + justify-content: center; + margin-top: 40px; + margin-bottom: 20px; +`; + +interface PaginationCompProps { + setCurrentPage: (page: number) => void; + setPageSize: (size: number) => void; + currentPage: number; + pageSize: number; + total: number; +} + +const PaginationComp = (props: PaginationCompProps) => { + const { + setCurrentPage, + setPageSize, + currentPage, + pageSize, + total, + } = props; + + const handlePageChange = (page: number, pageSize: number | undefined) => { + if (setCurrentPage) { + setCurrentPage(page); + } + }; + + const handlePageSizeChange = (current: number, size: number) => { + if (setPageSize) { + setPageSize(size); + } + }; + + return ( + + ); +}; + +export default PaginationComp; \ No newline at end of file diff --git a/client/packages/lowcoder/src/util/pagination/axios.ts b/client/packages/lowcoder/src/util/pagination/axios.ts new file mode 100644 index 0000000000..b634824e43 --- /dev/null +++ b/client/packages/lowcoder/src/util/pagination/axios.ts @@ -0,0 +1,115 @@ +import { FolderApi } from "@lowcoder-ee/api/folderApi"; +import ApplicationApi from "@lowcoder-ee/api/applicationApi"; +import { + fetchAppRequestType, + fetchDBRequestType, + fetchFolderRequestType, + fetchGroupUserRequestType, fetchOrgUserRequestType, + orgGroupRequestType +} from "@lowcoder-ee/util/pagination/type"; +import OrgApi from "@lowcoder-ee/api/orgApi"; +import { DatasourceApi } from "@lowcoder-ee/api/datasourceApi"; + +export const fetchFolderElements = async (request: fetchFolderRequestType) => { + try { + const response = await FolderApi.fetchFolderElementsPagination(request); + return { + success: true, + data: response.data.data, + total:response.data.total + }; + } catch (error) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + +export const fetchApplicationElements = async (request: fetchAppRequestType)=> { + try { + const response = await ApplicationApi.fetchAllApplicationsPagination(request); + return { + success: true, + data: response.data.data, + total: response.data.total + } + } catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + +export const fetchOrgGroups = async (request: orgGroupRequestType) => { + try{ + const response = await OrgApi.fetchGroupPagination(request); + return { + success: true, + data:response.data.data, + total:response.data.total + } + } + catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + +export const fetchDatasourcePagination = async (request: fetchDBRequestType)=> { + try { + const response = await DatasourceApi.fetchDatasourcePaginationByOrg(request); + return { + success: true, + data: response.data.data, + total: response.data.total + } + } catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + +export const fetchGroupUsrPagination = async (request: fetchGroupUserRequestType)=> { + try { + const response = await OrgApi.fetchGroupUsersPagination(request); + console.log("response", response); + return { + success: true, + data: response.data.data, + total: response.data.total + } + } catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} + +export const fetchOrgUsrPagination = async (request: fetchOrgUserRequestType)=> { + try { + const response = await OrgApi.fetchOrgUsersPagination(request); + return { + success: true, + data: response.data.data.members, + total: response.data.data?.total, + } + } catch (error: any) { + console.error('Failed to fetch data:', error); + return { + success: false, + error: error + }; + } +} diff --git a/client/packages/lowcoder/src/util/pagination/type.ts b/client/packages/lowcoder/src/util/pagination/type.ts new file mode 100644 index 0000000000..7fd8e41788 --- /dev/null +++ b/client/packages/lowcoder/src/util/pagination/type.ts @@ -0,0 +1,80 @@ +import {GroupUser, OrgUser} from "@lowcoder-ee/constants/orgConstants"; +import {ApiResponse} from "@lowcoder-ee/api/apiResponses"; + +type ApplicationType = { + [key: number]: string; // This allows numeric indexing +}; + +export interface GenericApiPaginationResponse { + total: number; + success: boolean; + code: number; + message: string; + data: T; +} +export interface GroupUsersPaginationResponse { + total: number; + success: boolean; + data: { + members: GroupUser[]; + visitorRole: string; + }; +} + +export interface OrgUsersPaginationResponse { + total: number; + success: boolean; + data: { + total: number; + members: OrgUser[]; + visitorRole: string; + }; +} + +export const ApplicationPaginationType: ApplicationType = { + 0: "", + 1: "APPLICATION", + 2: "MODULE", + 3: "NAVLAYOUT", + 4: "FOLDER", + 6: "MOBILETABLAYOUT", + 7: "NAVIGATION", +}; + +export interface fetchAppRequestType { + pageNum?: number; + pageSize?: number; + name?: string; + applicationType?: number; +} + +export interface fetchFolderRequestType { + pageNum?: number; + pageSize?: number; + name?: string; + applicationType?: string; +} + +export interface fetchDBRequestType { + orgId: string; + pageNum?: number; + pageSize?: number; + name?: string; + type?: string; +} + +export interface orgGroupRequestType{ + pageNum?: number; + pageSize?: number; +} +export interface fetchOrgUserRequestType { + orgId: string; + pageNum?: number; + pageSize?: number; +} + +export interface fetchGroupUserRequestType { + groupId: string; + pageNum?: number; + pageSize?: number; +} diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 2c2bc5c27d..5ecbbd579d 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 dded29c35e..548d6e439b 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 809decfd60..eabf2caf6f 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 fd4a79f824..f4e5b3fcf6 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 c47b399556..2d4aba44a4 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/organization/service/OrganizationServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java index 9a2bb24cc5..a1358b39f8 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/organization/service/OrganizationServiceImpl.java @@ -91,9 +91,6 @@ public Mono createDefault(User user, boolean isSuperAdmin) { if (Boolean.TRUE.equals(join)) { return Mono.empty(); } - OrganizationDomain organizationDomain = new OrganizationDomain(); - organizationDomain.setConfigs(List.of(DEFAULT_AUTH_CONFIG)); - organization.setOrganizationDomain(organizationDomain); return create(organization, user.getId(), isSuperAdmin); }); }); 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 6b800720cd..981000caf5 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 ed7079598c..3c91f7ce7a 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 4eed69ee2c..cef1198475 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 6b6d94a513..b5a6381d70 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 254e780373..cb0df92418 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 8674c62b5f..8d668c1b75 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/datasource/DatasourceController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java index 4d00716390..695245c415 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceController.java @@ -119,7 +119,7 @@ public Mono> getStructure(@PathVariable String */ @Override public Mono> listJsDatasourcePlugins(@RequestParam("appId") String applicationId, @RequestParam(required = false) String name, @RequestParam(required = false) String type, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "0") int pageSize) { String objectId = gidService.convertApplicationIdToObjectId(applicationId); return fluxToPageResponseView(pageNum, pageSize, datasourceApiService.listJsDatasourcePlugins(objectId, name, type)); @@ -142,7 +142,7 @@ public Mono>> getPluginDynamicConfig( @SneakyThrows @Override public Mono> listOrgDataSources(@RequestParam(name = "orgId") String orgId, @RequestParam(required = false) String name, @RequestParam(required = false) String type, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "0") int pageSize) { if (StringUtils.isBlank(orgId)) { return ofError(BizError.INVALID_PARAMETER, "ORG_ID_EMPTY"); @@ -153,7 +153,7 @@ public Mono> listOrgDataSources(@RequestParam(name = "orgId" @Override public Mono> listAppDataSources(@RequestParam(name = "appId") String applicationId, @RequestParam(required = false) String name, @RequestParam(required = false) String type, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "0") int pageSize) { if (StringUtils.isBlank(applicationId)) { return ofError(BizError.INVALID_PARAMETER, "INVALID_APP_ID"); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java index d3608533df..775d702290 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/datasource/DatasourceEndpoints.java @@ -101,7 +101,7 @@ public Mono> getStructure(@PathVariable String ) @GetMapping("/jsDatasourcePlugins") public Mono> listJsDatasourcePlugins(@RequestParam("appId") String applicationId, @RequestParam(required = false) String name, @RequestParam(required = false) String type, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "0") int pageSize); /** @@ -127,7 +127,7 @@ public Mono>> getPluginDynamicConfig( @JsonView(JsonViews.Public.class) @GetMapping("/listByOrg") public Mono> listOrgDataSources(@RequestParam(name = "orgId") String orgId, @RequestParam String name, @RequestParam String type, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "0") int pageSize); @Operation( @@ -140,7 +140,7 @@ public Mono> listOrgDataSources(@RequestParam(name = "orgId" @JsonView(JsonViews.Public.class) @GetMapping("/listByApp") public Mono> listAppDataSources(@RequestParam(name = "appId") String applicationId, @RequestParam String name, @RequestParam String type, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "0") int 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 31cf494948..24a77dd290 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 43e5ce785f..2c62790840 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/query/LibraryQueryController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java index be0e7de68e..a7a5a320d7 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryController.java @@ -46,7 +46,7 @@ public Mono>> dropDownList(@Request @Override public Mono> list(@RequestParam(required = false, defaultValue = "") String name, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "100") int pageSize) { var flux = libraryQueryApiService.listLibraryQueries(name) .flatMapMany(Flux::fromIterable); diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryEndpoints.java index c4acd37497..bf4b8f161b 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryEndpoints.java @@ -40,7 +40,7 @@ public interface LibraryQueryEndpoints ) @GetMapping("/listByOrg") public Mono> list(@RequestParam(required = false, defaultValue = "") String name, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "100") int pageSize); @Operation( diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordController.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordController.java index 31a1b8b4dd..9db6a9ea26 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordController.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordController.java @@ -30,7 +30,7 @@ public Mono delete(@PathVariable String libraryQueryRecordId) { @Override public Mono> getByLibraryQueryId(@RequestParam(name = "libraryQueryId") String libraryQueryId, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "100") int pageSize) { return fluxToPageResponseView(pageNum, pageSize, libraryQueryRecordApiService.getByLibraryQueryId(libraryQueryId).flatMapMany(Flux::fromIterable)); } diff --git a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordEndpoints.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordEndpoints.java index 7fb642fb0f..9f41f380d2 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordEndpoints.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/api/query/LibraryQueryRecordEndpoints.java @@ -41,7 +41,7 @@ public interface LibraryQueryRecordEndpoints ) @GetMapping("/listByLibraryQueryId") public Mono> getByLibraryQueryId(@RequestParam(name = "libraryQueryId") String libraryQueryId, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "100") int 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 d478bcfc28..a3ed463f74 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, @@ -119,7 +119,7 @@ public Mono>> getOrgGroups(@RequestParam(r @Override public Mono> getGroupMembers(@PathVariable String groupId, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "100") int pageSize) { String objectId = gidService.convertGroupIdToObjectId(groupId); return groupApiService.getGroupMembers(objectId, pageNum, pageSize) 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 4f08253336..89e294628d 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( @@ -74,7 +74,7 @@ public Mono>> getOrgGroups(@RequestParam(r ) @GetMapping("/{groupId}/members") public Mono> getGroupMembers(@PathVariable String groupId, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "100") int 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 b0acc8cf1a..d43676ba53 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())) @@ -90,7 +90,7 @@ public Mono> deleteLogo(@PathVariable String orgId) { @Override public Mono> getOrgMembers(@PathVariable String orgId, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "1000") int pageSize) { String id = gidService.convertOrganizationIdToObjectId(orgId); return orgApiService.getOrganizationMembers(id, pageNum, pageSize) 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 7340120336..8fc9d55988 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( @@ -95,7 +95,7 @@ public Mono> uploadLogo(@PathVariable String orgId, ) @GetMapping("/{orgId}/members") public Mono> getOrgMembers(@PathVariable String orgId, - @RequestParam(required = false, defaultValue = "0") int pageNum, + @RequestParam(required = false, defaultValue = "1") int pageNum, @RequestParam(required = false, defaultValue = "1000") int 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 051c3e006a..03141d6bb0 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 ddf0422ab6..a51a74e09a 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/migrations/job/MigrateAuthConfigJobImpl.java b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/MigrateAuthConfigJobImpl.java index d866159595..a89eb4480a 100644 --- a/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/MigrateAuthConfigJobImpl.java +++ b/server/api-service/lowcoder-server/src/main/java/org/lowcoder/runner/migrations/job/MigrateAuthConfigJobImpl.java @@ -8,6 +8,7 @@ import org.lowcoder.sdk.auth.AbstractAuthConfig; import org.lowcoder.sdk.config.AuthProperties; import org.lowcoder.sdk.config.CommonConfig; +import org.lowcoder.sdk.constants.AuthSourceConstants; import org.lowcoder.sdk.constants.WorkspaceMode; import org.lowcoder.sdk.util.IDUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -57,6 +58,6 @@ protected void setAuthConfigs2OrganizationDomain(Organization organization, List organization.setOrganizationDomain(domain); } authConfigs.forEach(abstractAuthConfig -> abstractAuthConfig.setId(IDUtils.generate())); - domain.setConfigs(authConfigs); + domain.setConfigs(authConfigs.stream().filter(authConfig -> !authConfig.getSource().equals(AuthSourceConstants.EMAIL)).toList()); } } 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 2fa5163799..28108f51a0 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 fb7109134d..81c0cb56d1 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());