diff --git a/client/packages/lowcoder/src/api/datasourceApi.ts b/client/packages/lowcoder/src/api/datasourceApi.ts index 1be29e646..278015a12 100644 --- a/client/packages/lowcoder/src/api/datasourceApi.ts +++ b/client/packages/lowcoder/src/api/datasourceApi.ts @@ -187,6 +187,10 @@ export class DatasourceApi extends Api { return Api.get(DatasourceApi.url + `/listByOrg?orgId=${orgId}`, {...res}); } + static getDatasourceById(id: string): AxiosPromise> { + return Api.get(`${DatasourceApi.url}/${id}`); + } + static createDatasource( datasourceConfig: Partial ): AxiosPromise> { diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/FolderView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/FolderView.tsx index 695b932a9..887a74cc1 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/FolderView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/FolderView.tsx @@ -9,6 +9,8 @@ import { Helmet } from "react-helmet"; import { trans } from "i18n"; import {ApplicationPaginationType} from "@lowcoder-ee/util/pagination/type"; import {fetchFolderElements} from "@lowcoder-ee/util/pagination/axios"; +import { fetchFolderElements as fetchFolderElementsRedux } from "../../redux/reduxActions/folderActions"; +import { getUser } from "../../redux/selectors/usersSelectors"; function getBreadcrumbs( folder: FolderMeta, @@ -52,6 +54,7 @@ export function FolderView() { const element = useSelector(folderElementsSelector); const allFolders = useSelector(foldersSelector); + const user = useSelector(getUser); const folder = allFolders.filter((f) => f.folderId === folderId)[0] || {}; const breadcrumbs = getBreadcrumbs(folder, allFolders, [ @@ -61,6 +64,13 @@ export function FolderView() { }, ]); + // Fetch folder data for breadcrumbs if not available + useEffect(() => { + if (allFolders.length === 0 && user.currentOrgId) { + dispatch(fetchFolderElementsRedux({})); + } + }, [allFolders.length, user.currentOrgId, dispatch]); + useEffect( () => { try{ fetchFolderElements({ diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/HomeResOptions.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/HomeResOptions.tsx index 99244d7fc..38e4b6895 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/HomeResOptions.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/HomeResOptions.tsx @@ -3,8 +3,6 @@ import { HomeResTypeEnum } from "../../types/homeRes"; import { exportApplicationAsJSONFile } from "./components/AppImport"; import { CustomModal, EditPopover, EditPopoverItemType, PointIcon } from "lowcoder-design"; import { HomeResInfo } from "../../util/homeResUtils"; -import { recycleApplication } from "../../redux/reduxActions/applicationActions"; -import { deleteFolder } from "../../redux/reduxActions/folderActions"; import { useDispatch } from "react-redux"; import React, { useState } from "react"; import styled from "styled-components"; @@ -13,6 +11,9 @@ import { useParams } from "react-router-dom"; import { AppTypeEnum } from "constants/applicationConstants"; import { CopyModal } from "pages/common/copyModal"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; +import ApplicationApi from "../../api/applicationApi"; +import { FolderApi } from "../../api/folderApi"; +import { ReduxActionTypes } from "constants/reduxActionConstants"; const PopoverIcon = styled(PointIcon)` cursor: pointer; @@ -80,23 +81,20 @@ export const HomeResOptions = (props: { type: HomeResInfo[res.type].name, name: {res.name}, }), - onConfirm: () =>{ - new Promise((resolve, reject) => { - dispatch( - recycleApplication( - { applicationId: res.id, folderId: folderId }, - () => { - messageInstance.success(trans("success")); - resolve(true); - }, - () => reject() - ) - ); + onConfirm: async () => { + try { + await ApplicationApi.recycleApplication({ + applicationId: res.id, + folderId: folderId || "" + }); + messageInstance.success(trans("success")); setTimeout(() => { setModify(!modify); }, 200); - }) - + } catch (error) { + console.error("Failed to recycle application:", error); + messageInstance.error("Failed to delete application"); + } }, confirmBtnType: "delete", okText: trans("home.moveToTrash"), @@ -122,22 +120,27 @@ export const HomeResOptions = (props: { type: HomeResInfo[res.type].name.toLowerCase(), name: {res.name}, }), - onConfirm: () =>{ - new Promise((resolve, reject) => { - dispatch( - deleteFolder( - { folderId: res.id, parentFolderId: folderId }, - () => { - messageInstance.success(trans("home.deleteSuccessMsg")); - resolve(true); - }, - () => reject() - ) - ); - }) - setTimeout(() => { - setModify(!modify); - }, 200); + onConfirm: async () => { + try { + await FolderApi.deleteFolder({ + folderId: res.id, + parentFolderId: folderId || "" + }); + + // Update Redux state to remove deleted folder from dropdown + dispatch({ + type: ReduxActionTypes.DELETE_FOLDER_SUCCESS, + payload: { folderId: res.id, parentFolderId: folderId || "" } + }); + + messageInstance.success(trans("home.deleteSuccessMsg")); + setTimeout(() => { + setModify(!modify); + }, 200); + } catch (error) { + console.error("Failed to delete folder:", error); + messageInstance.error("Failed to delete folder"); + } }, confirmBtnType: "delete", okText: trans("delete"), diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/TrashTableView.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/TrashTableView.tsx index 11323e56d..54d93835d 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/TrashTableView.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/TrashTableView.tsx @@ -5,11 +5,11 @@ import styled from "styled-components"; import { useDispatch } from "react-redux"; import { HomeResInfo } from "../../util/homeResUtils"; import { HomeResTypeEnum } from "../../types/homeRes"; -import { deleteApplication, restoreApplication } from "../../redux/reduxActions/applicationActions"; import { HomeRes } from "./HomeLayout"; import { trans, transToNode } from "../../i18n"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { BrandedIcon } from "@lowcoder-ee/components/BrandedIcon"; +import ApplicationApi from "../../api/applicationApi"; const OperationWrapper = styled.div` display: flex; @@ -123,17 +123,18 @@ export const TrashTableView = (props: { resources: HomeRes[] , setModify: any, m style={{ padding: "0 8px", width: "fit-content", minWidth: "52px" }} buttonType={"blue"} className={"home-datasource-edit-button"} - onClick={() =>{ - dispatch( - restoreApplication({ applicationId: item.id }, () => { - messageInstance.success(trans("home.recoverSuccessMsg")); - }) - ) + onClick={async () => { + try { + await ApplicationApi.restoreApplication({ applicationId: item.id }); + messageInstance.success(trans("home.recoverSuccessMsg")); setTimeout(() => { - setModify(!modify); + setModify(!modify); }, 200); - } - } + } catch (error) { + console.error("Failed to restore application:", error); + messageInstance.error("Failed to restore application"); + } + }} > {trans("recover")} @@ -148,27 +149,21 @@ export const TrashTableView = (props: { resources: HomeRes[] , setModify: any, m type: HomeResInfo[item.type].name.toLowerCase(), name: {item.name}, }), - onConfirm: () =>{ - new Promise((resolve, reject) => { - dispatch( - deleteApplication( - { applicationId: item.id }, - () => { - messageInstance.success(trans("home.deleteSuccessMsg")); - resolve(true); - }, - () => reject() - ) - ); - }) + onConfirm: async () => { + try { + await ApplicationApi.deleteApplication({ applicationId: item.id }); + messageInstance.success(trans("home.deleteSuccessMsg")); setTimeout(() => { - setModify(!modify); + setModify(!modify); }, 200); + } catch (error) { + console.error("Failed to delete application:", error); + messageInstance.error("Failed to delete application permanently"); + } }, confirmBtnType: "delete", okText: trans("delete"), }) - } style={{ marginLeft: "12px", width: "76px" }} > diff --git a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx index 5987d097d..14bf9df2e 100644 --- a/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx +++ b/client/packages/lowcoder/src/pages/ApplicationV2/index.tsx @@ -124,15 +124,6 @@ export default function ApplicationHome() { setIsPreloadCompleted(true); }, [org, orgHomeId]); - useEffect(() => { - // Check if we need to fetch data (either no folders or no applications) - if (allFoldersCount !== 0 && allAppCount !== 0) { - return; - } - - user.currentOrgId && dispatch(fetchFolderElements({})); - }, [dispatch, allFoldersCount, allAppCount, user.currentOrgId]); - if (fetchingUser || !isPreloadCompleted) { return ; } diff --git a/client/packages/lowcoder/src/pages/datasource/datasourceEditPage.tsx b/client/packages/lowcoder/src/pages/datasource/datasourceEditPage.tsx index 7e033cb7a..9f8de01aa 100644 --- a/client/packages/lowcoder/src/pages/datasource/datasourceEditPage.tsx +++ b/client/packages/lowcoder/src/pages/datasource/datasourceEditPage.tsx @@ -1,20 +1,25 @@ import styled from "styled-components"; import history from "../../util/history"; import { default as Button } from "antd/es/button"; -import { useCallback, useMemo, useState } from "react"; +import { Spin } from "antd"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { CopyTextButton, DocIcon, PackUpIcon, TacoButton } from "lowcoder-design"; import { useDatasourceForm } from "./form/useDatasourceForm"; import { useParams } from "react-router-dom"; import { DATASOURCE_URL } from "../../constants/routesURL"; import { useSelector } from "react-redux"; -import { getDataSource, getDataSourceTypes } from "../../redux/selectors/datasourceSelectors"; +import { getDataSourceTypes } from "../../redux/selectors/datasourceSelectors"; import { trans } from "i18n"; import { DatasourceType } from "@lowcoder-ee/constants/queryConstants"; import { getDatasourceTutorial } from "@lowcoder-ee/util/tutorialUtils"; import { getDataSourceFormManifest } from "./getDataSourceFormManifest"; import DataSourceIcon from "components/DataSourceIcon"; import { Helmet } from "react-helmet"; - +import { DatasourceApi } from "@lowcoder-ee/api/datasourceApi"; +import { DatasourceInfo } from "@lowcoder-ee/api/datasourceApi"; +import { GenericApiResponse } from "../../api/apiResponses"; +import { Datasource } from "@lowcoder-ee/constants/datasourceConstants"; +import { AxiosResponse } from "axios"; const Wrapper = styled.div` display: flex; justify-content: center; @@ -154,16 +159,44 @@ type DatasourcePathParams = { export const DatasourceEditPage = () => { const { datasourceId, datasourceType } = useParams(); - const datasourceList = useSelector(getDataSource); const datasourceTypes = useSelector(getDataSourceTypes); const [isReady, setIsReady] = useState(true); - const datasourceInfo = useMemo(() => { + + const [datasourceInfo, setDatasourceInfo] = useState(); + const [loading, setLoading] = useState(false); + + // Fetch individual datasource when editing + useEffect(() => { if (!datasourceId) { - return undefined; + setDatasourceInfo(undefined); + return; } - return datasourceList.find((info) => info.datasource.id === datasourceId); - }, [datasourceId, datasourceList]); + + const fetchDatasource = async () => { + setLoading(true); + try { + const response: AxiosResponse> = await DatasourceApi.getDatasourceById(datasourceId); + if (response.data.success) { + // Transform to DatasourceInfo format + setDatasourceInfo({ + datasource: response.data.data, + edit: true, // Assume editable since user reached edit page + }); + } else { + console.error('API returned error:', response.data); + setDatasourceInfo(undefined); + } + } catch (error: any) { + console.error('Failed to fetch datasource:', error); + setDatasourceInfo(undefined); + } finally { + setLoading(false); + } + }; + + fetchDatasource(); + }, [datasourceId]); const dataSourceTypeInfo = useMemo(() => { if (datasourceId) { @@ -181,6 +214,26 @@ export const DatasourceEditPage = () => { setIsReady(isReady); }, []); + // Show loading state while fetching datasource + if (loading) { + return ( + + +
+ +
+
+
+ ); + } + if (!finalDataSourceType) { return null; } diff --git a/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx b/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx index 2d7386fa0..61eb621b2 100644 --- a/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx +++ b/client/packages/lowcoder/src/pages/datasource/datasourceList.tsx @@ -1,8 +1,8 @@ import styled from "styled-components"; import { EditPopover, PointIcon, Search, TacoButton } from "lowcoder-design"; -import React, {useEffect, useState} from "react"; +import {useEffect, useState} from "react"; import { useDispatch, useSelector } from "react-redux"; -import { getDataSource, getDataSourceLoading, getDataSourceTypesMap } from "../../redux/selectors/datasourceSelectors"; +import { getDataSourceTypesMap } from "../../redux/selectors/datasourceSelectors"; import { deleteDatasource } from "../../redux/reduxActions/datasourceActions"; import { isEmpty } from "lodash"; import history from "../../util/history"; @@ -113,7 +113,6 @@ export const DatasourceList = () => { const [modify, setModify] = useState(false); const currentUser = useSelector(getUser); const orgId = currentUser.currentOrgId; - const datasourceLoading = useSelector(getDataSourceLoading); const plugins = useSelector(getDataSourceTypesMap); interface ElementsState { elements: DatasourceInfo[]; @@ -123,6 +122,7 @@ export const DatasourceList = () => { const [elements, setElements] = useState({ elements: [], total: 0 }); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); + const [paginationLoading, setPaginationLoading] = useState(false); useEffect(()=> { const timer = setTimeout(() => { @@ -133,6 +133,7 @@ export const DatasourceList = () => { }, [searchValue]) useEffect( () => { + setPaginationLoading(true); fetchDatasourcePagination( { orgId: orgId, @@ -146,6 +147,8 @@ export const DatasourceList = () => { } else console.error("ERROR: fetchFolderElements", result.error) + }).finally(() => { + setPaginationLoading(false); }) }, [currentPage, pageSize, searchValues, modify] ) @@ -195,7 +198,7 @@ export const DatasourceList = () => { }} rowClassName={(record: any) => (!record.edit ? "datasource-can-not-edit" : "")} diff --git a/client/packages/lowcoder/src/pages/datasource/index.tsx b/client/packages/lowcoder/src/pages/datasource/index.tsx index fe965aba6..9364af391 100644 --- a/client/packages/lowcoder/src/pages/datasource/index.tsx +++ b/client/packages/lowcoder/src/pages/datasource/index.tsx @@ -8,26 +8,19 @@ import { } from "../../constants/routesURL"; import React, { useEffect } from "react"; import { isEmpty } from "lodash"; -import { fetchDatasource, fetchDataSourceTypes } from "../../redux/reduxActions/datasourceActions"; +import { fetchDataSourceTypes } from "../../redux/reduxActions/datasourceActions"; import { useDispatch, useSelector } from "react-redux"; import { getUser } from "../../redux/selectors/usersSelectors"; -import { getDataSource, getDataSourceTypes } from "../../redux/selectors/datasourceSelectors"; +import { getDataSourceTypes } from "../../redux/selectors/datasourceSelectors"; export const DatasourceHome = () => { const dispatch = useDispatch(); - const datasourceList = useSelector(getDataSource); const datasourceTypes = useSelector(getDataSourceTypes); const currentUser = useSelector(getUser); const orgId = currentUser.currentOrgId; - useEffect(() => { - if (isEmpty(orgId) || datasourceList.length !== 0) { - return; - } - dispatch(fetchDatasource({ organizationId: orgId })); - }, [dispatch, datasourceList.length, orgId]); useEffect(() => { if (isEmpty(orgId) || datasourceTypes.length !== 0) { diff --git a/client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts b/client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts index 3142ca13d..f1a04ea2c 100644 --- a/client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts +++ b/client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts @@ -25,7 +25,9 @@ const initialState: UsersReduxState = { workspaces: { items: [], totalCount: 0, - currentOrg: null + currentOrg: null, + loading: false, + isFetched: false, } }; @@ -198,6 +200,14 @@ const usersReducer = createReducer(initialState, { apiKeys: action.payload, }), + [ReduxActionTypes.FETCH_WORKSPACES_INIT]: (state: UsersReduxState) => ({ + ...state, + workspaces: { + ...state.workspaces, + loading: true, + }, + }), + [ReduxActionTypes.FETCH_WORKSPACES_SUCCESS]: ( state: UsersReduxState, @@ -208,7 +218,9 @@ const usersReducer = createReducer(initialState, { items: action.payload.isLoadMore ? [...state.workspaces.items, ...action.payload.items] // Append for load more : action.payload.items, // Replace for new search/initial load - totalCount: action.payload.totalCount + totalCount: action.payload.totalCount, + isFetched: true, + loading: false, } }), @@ -234,6 +246,8 @@ export interface UsersReduxState { items: Org[]; // Current page of workspaces totalCount: number; // Total workspaces available currentOrg: Org | null; + loading: boolean; + isFetched: boolean; }; } diff --git a/client/packages/lowcoder/src/redux/sagas/userSagas.ts b/client/packages/lowcoder/src/redux/sagas/userSagas.ts index d0dfdba06..b19e2b1a6 100644 --- a/client/packages/lowcoder/src/redux/sagas/userSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/userSagas.ts @@ -77,8 +77,7 @@ export function* getUserSaga() { type: ReduxActionTypes.FETCH_USER_DETAILS_SUCCESS, payload: user, }); - // fetch all workspaces and store in redux - yield put(fetchWorkspacesAction(1, 10)); + } } catch (error: any) { yield put({ diff --git a/client/packages/lowcoder/src/util/context/EnterpriseContext.tsx b/client/packages/lowcoder/src/util/context/EnterpriseContext.tsx index ba8e911c9..61b3097dc 100644 --- a/client/packages/lowcoder/src/util/context/EnterpriseContext.tsx +++ b/client/packages/lowcoder/src/util/context/EnterpriseContext.tsx @@ -26,13 +26,12 @@ export const EnterpriseProvider: React.FC = ({ children }) => { // Fetch the enterprise license only if we're in an EE environment dispatch(fetchEnterpriseLicense()); dispatch(fetchEnvironments()); - dispatch(fetchBrandingSetting({ orgId: user.currentOrgId, fallbackToGlobal: true })) } else { // Set the state to false for non-EE environments // setEEActiveState(false); setIsEnterpriseActive(false); } - }, [dispatch, user.currentOrgId]); + }, [dispatch]); useEffect(() => { if (isEEEnvironment()) { @@ -41,6 +40,12 @@ export const EnterpriseProvider: React.FC = ({ children }) => { setIsEnterpriseActive(isEnterpriseActiveRedux); } }, [isEnterpriseActiveRedux]); + + useEffect(() => { + if (isEEEnvironment()) { + dispatch(fetchBrandingSetting({ orgId: user.currentOrgId, fallbackToGlobal: true })) + } + }, [dispatch, user.currentOrgId]); return ( diff --git a/client/packages/lowcoder/src/util/useWorkspaceManager.ts b/client/packages/lowcoder/src/util/useWorkspaceManager.ts index 5c5cafee0..fe2379769 100644 --- a/client/packages/lowcoder/src/util/useWorkspaceManager.ts +++ b/client/packages/lowcoder/src/util/useWorkspaceManager.ts @@ -1,8 +1,9 @@ import { useReducer, useEffect, useCallback, useMemo } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { debounce } from 'lodash'; import { Org } from 'constants/orgConstants'; import { getWorkspaces } from 'redux/selectors/orgSelectors'; +import { fetchWorkspacesAction } from 'redux/reduxActions/orgActions'; import UserApi from 'api/userApi'; // State interface for the workspace manager @@ -73,6 +74,7 @@ export function useWorkspaceManager({ }: UseWorkspaceManagerOptions) { // Get workspaces from Redux const workspaces = useSelector(getWorkspaces); + const reduxDispatch = useDispatch(); // Initialize reducer with Redux total count const [state, dispatch] = useReducer(workspaceReducer, { @@ -81,6 +83,14 @@ export function useWorkspaceManager({ }); + + /* ----- first-time fetch ------------------------------------------------ */ + useEffect(() => { + if (!workspaces.isFetched && !workspaces.loading) { + reduxDispatch(fetchWorkspacesAction(1, pageSize)); + } + }, [workspaces.isFetched, workspaces.loading, pageSize, reduxDispatch]); + // API call to fetch workspaces (memoized for stable reference) const fetchWorkspacesPage = useCallback( async (page: number, search?: string) => { @@ -177,7 +187,7 @@ export function useWorkspaceManager({ // State searchTerm: state.searchTerm, currentPage: state.currentPage, - isLoading: state.isLoading, + isLoading: state.isLoading || workspaces.loading, displayWorkspaces, totalCount: currentTotalCount,