diff --git a/client/packages/lowcoder/src/constants/reduxActionConstants.ts b/client/packages/lowcoder/src/constants/reduxActionConstants.ts index 821470ac56..14eae5c70b 100644 --- a/client/packages/lowcoder/src/constants/reduxActionConstants.ts +++ b/client/packages/lowcoder/src/constants/reduxActionConstants.ts @@ -11,12 +11,6 @@ export const ReduxActionTypes = { FETCH_API_KEYS_SUCCESS: "FETCH_API_KEYS_SUCCESS", MOVE_TO_FOLDER2_SUCCESS: "MOVE_TO_FOLDER2_SUCCESS", - /* workspace RELATED */ - FETCH_WORKSPACES_INIT: "FETCH_WORKSPACES_INIT", - FETCH_WORKSPACES_SUCCESS: "FETCH_WORKSPACES_SUCCESS", - - - /* plugin RELATED */ FETCH_DATA_SOURCE_TYPES: "FETCH_DATA_SOURCE_TYPES", FETCH_DATA_SOURCE_TYPES_SUCCESS: "FETCH_DATA_SOURCE_TYPES_SUCCESS", diff --git a/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx b/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx index 0bd8a4c547..8b8180b4e8 100644 --- a/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx +++ b/client/packages/lowcoder/src/pages/common/WorkspaceSection.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { Input, Pagination, Spin } from 'antd'; @@ -198,8 +198,19 @@ export default function WorkspaceSectionComponent({ handleSearchChange, handlePageChange, pageSize, + refetch, } = useWorkspaceManager({}); + // Refetch list when orgs change (create/delete) + const orgsCount = user.orgs.length; + const prevOrgsCount = useRef(orgsCount); + useEffect(() => { + if (prevOrgsCount.current !== orgsCount) { + prevOrgsCount.current = orgsCount; + refetch(); + } + }, [orgsCount, refetch]); + // Early returns for better performance if (!showSwitchOrg(user, sysConfig)) return null; diff --git a/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx b/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx index 78b48b185f..c9fb6e90a7 100644 --- a/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx +++ b/client/packages/lowcoder/src/pages/setting/organization/orgList.tsx @@ -24,7 +24,7 @@ import { getUser } from "redux/selectors/usersSelectors"; import { getOrgCreateStatus } from "redux/selectors/orgSelectors"; import { useWorkspaceManager } from "util/useWorkspaceManager"; import { Org } from "constants/orgConstants"; -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { SwapOutlined } from "@ant-design/icons"; import dayjs from "dayjs"; @@ -195,10 +195,21 @@ function OrganizationSetting() { handleSearchChange, handlePageChange, pageSize, + refetch, } = useWorkspaceManager({ pageSize: 10 }); + // Refetch list when orgs change (create/delete) + const orgsCount = user.orgs.length; + const prevOrgsCount = useRef(orgsCount); + useEffect(() => { + if (prevOrgsCount.current !== orgsCount) { + prevOrgsCount.current = orgsCount; + refetch(); + } + }, [orgsCount, refetch]); + // Show all organizations with role information diff --git a/client/packages/lowcoder/src/redux/reducers/uiReducers/applicationReducer.ts b/client/packages/lowcoder/src/redux/reducers/uiReducers/applicationReducer.ts index bf5369b869..686838a5ad 100644 --- a/client/packages/lowcoder/src/redux/reducers/uiReducers/applicationReducer.ts +++ b/client/packages/lowcoder/src/redux/reducers/uiReducers/applicationReducer.ts @@ -11,6 +11,7 @@ import { UpdateAppMetaPayload, UpdateAppPermissionPayload, } from "redux/reduxActions/applicationActions"; +import { UpdateOrgPayload } from "redux/reduxActions/orgActions"; import { createReducer } from "util/reducerUtils"; import { ApplicationDetail, @@ -70,6 +71,15 @@ const usersReducer = createReducer(initialState, { }, }, }), + [ReduxActionTypes.UPDATE_ORG_SUCCESS]: ( + state: ApplicationReduxState, + action: ReduxAction + ): ApplicationReduxState => ({ + ...state, + homeOrg: state.homeOrg && state.homeOrg.id === action.payload.id + ? { ...state.homeOrg, ...(action.payload.orgName && { name: action.payload.orgName }) } + : state.homeOrg, + }), [ReduxActionTypes.FETCH_HOME_DATA_SUCCESS]: ( state: ApplicationReduxState, action: ReduxAction diff --git a/client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts b/client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts index f1a04ea2cc..be4b3a1dd0 100644 --- a/client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts +++ b/client/packages/lowcoder/src/redux/reducers/uiReducers/usersReducer.ts @@ -1,4 +1,3 @@ -import { Org } from "@lowcoder-ee/constants/orgConstants"; import { ReduxAction, ReduxActionErrorTypes, @@ -22,14 +21,6 @@ const initialState: UsersReduxState = { rawCurrentUser: defaultCurrentUser, profileSettingModalVisible: false, apiKeys: [], - workspaces: { - items: [], - totalCount: 0, - currentOrg: null, - loading: false, - isFetched: false, - - } }; const usersReducer = createReducer(initialState, { @@ -199,31 +190,6 @@ const usersReducer = createReducer(initialState, { ...state, apiKeys: action.payload, }), - - [ReduxActionTypes.FETCH_WORKSPACES_INIT]: (state: UsersReduxState) => ({ - ...state, - workspaces: { - ...state.workspaces, - loading: true, - }, - }), - - - [ReduxActionTypes.FETCH_WORKSPACES_SUCCESS]: ( - state: UsersReduxState, - action: ReduxAction<{ items: Org[], totalCount: number, isLoadMore?: boolean }> - ) => ({ - ...state, - workspaces: { - 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, - isFetched: true, - loading: false, - } - }), - }); export interface UsersReduxState { @@ -239,16 +205,6 @@ export interface UsersReduxState { error: string; profileSettingModalVisible: boolean; apiKeys: Array; - - // NEW state for workspaces - // NEW: Separate workspace state - workspaces: { - items: Org[]; // Current page of workspaces - totalCount: number; // Total workspaces available - currentOrg: Org | null; - loading: boolean; - isFetched: boolean; - }; } export default usersReducer; diff --git a/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts b/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts index 2bb8374ca9..d8e745df85 100644 --- a/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts +++ b/client/packages/lowcoder/src/redux/reduxActions/orgActions.ts @@ -161,7 +161,6 @@ export const updateOrgSuccess = (payload: UpdateOrgPayload) => { }; -// till now export type OrgAPIUsagePayload = { apiUsage: number, }; @@ -181,7 +180,6 @@ export const fetchAPIUsageActionSuccess = (payload: OrgAPIUsagePayload) => { }; }; -// last month export type OrgLastMonthAPIUsagePayload = { lastMonthApiUsage: number, }; @@ -199,14 +197,4 @@ export const fetchLastMonthAPIUsageActionSuccess = (payload: OrgLastMonthAPIUsag type: ReduxActionTypes.FETCH_ORG_LAST_MONTH_API_USAGE_SUCCESS, payload: payload, }; -}; - -export const fetchWorkspacesAction = (page: number = 1,pageSize: number = 20, search?: string, isLoadMore?: boolean) => ({ - type: ReduxActionTypes.FETCH_WORKSPACES_INIT, - payload: { page, pageSize, search, isLoadMore } -}); - -export const loadMoreWorkspacesAction = (page: number, search?: string) => ({ - type: ReduxActionTypes.FETCH_WORKSPACES_INIT, - payload: { page, search, isLoadMore: true } -}); \ No newline at end of file +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts index 9ac80186f2..03bf406057 100644 --- a/client/packages/lowcoder/src/redux/sagas/orgSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/orgSagas.ts @@ -1,6 +1,6 @@ import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; -import { ApiResponse, FetchGroupApiResponse, GenericApiResponse } from "api/apiResponses"; +import { ApiResponse, FetchGroupApiResponse } from "api/apiResponses"; import OrgApi, { CreateOrgResponse, GroupUsersResponse, OrgAPIUsageResponse, OrgUsersResponse } from "api/orgApi"; import { AxiosResponse } from "axios"; import { OrgGroup } from "constants/orgConstants"; @@ -25,14 +25,11 @@ import { fetchLastMonthAPIUsageActionSuccess, UpdateUserGroupRolePayload, UpdateUserOrgRolePayload, - fetchWorkspacesAction, } from "redux/reduxActions/orgActions"; import { getUser } from "redux/selectors/usersSelectors"; import { validateResponse } from "api/apiUtils"; import { User } from "constants/userConstants"; import { getUserSaga } from "redux/sagas/userSagas"; -import { GetMyOrgsResponse } from "@lowcoder-ee/api/userApi"; -import UserApi from "@lowcoder-ee/api/userApi"; export function* updateGroupSaga(action: ReduxAction) { try { @@ -264,14 +261,10 @@ export function* createOrgSaga(action: ReduxAction<{ orgName: string }>) { ); const isValidResponse: boolean = validateResponse(response); if (isValidResponse) { - // update org list yield call(getUserSaga); - // Refetch workspaces to update the profile dropdown - yield put(fetchWorkspacesAction(1, 10)); - yield put({ - type: ReduxActionTypes.CREATE_ORG_SUCCESS, - }); - + yield put({ + type: ReduxActionTypes.CREATE_ORG_SUCCESS, + }); } } catch (error: any) { yield put({ @@ -293,8 +286,6 @@ export function* deleteOrgSaga(action: ReduxAction<{ orgId: string }>) { orgId: action.payload.orgId, }, }); - // Refetch workspaces to update the profile dropdown - yield put(fetchWorkspacesAction(1, 10)); } } catch (error: any) { messageInstance.error(error.message); @@ -308,8 +299,6 @@ export function* updateOrgSaga(action: ReduxAction) { const isValidResponse: boolean = validateResponse(response); if (isValidResponse) { yield put(updateOrgSuccess(action.payload)); - // Refetch workspaces to update the profile dropdown - yield put(fetchWorkspacesAction(1, 10)); } } catch (error: any) { messageInstance.error(error.message); @@ -353,47 +342,6 @@ export function* fetchLastMonthAPIUsageSaga(action: ReduxAction<{ } } -// fetch my orgs -// In userSagas.ts -export function* fetchWorkspacesSaga(action: ReduxAction<{page: number, pageSize: number, search?: string, isLoadMore?: boolean}>) { - try { - const { page, pageSize, search, isLoadMore } = action.payload; - - const response: AxiosResponse = yield call( - UserApi.getMyOrgs, - page, // pageNum - pageSize, // pageSize (changed to 5 for testing) - search // orgName - ); - - if (validateResponse(response)) { - const apiData = response.data.data; - - // Transform orgId/orgName to match Org interface - const transformedItems = apiData.data - .filter(item => item.orgView && item.orgView.orgId) - .map(item => ({ - id: item.orgView.orgId, - name: item.orgView.orgName, - createdAt: item.orgView.createdAt, - updatedAt: item.orgView.updatedAt, - isCurrentOrg: item.isCurrentOrg, - })); - - yield put({ - type: ReduxActionTypes.FETCH_WORKSPACES_SUCCESS, - payload: { - items: transformedItems, - totalCount: apiData.total, - isLoadMore: isLoadMore || false - } - }); - } - } catch (error: any) { - console.error('Error fetching workspaces:', error); - } -} - export default function* orgSagas() { yield all([ takeLatest(ReduxActionTypes.UPDATE_GROUP_INFO, updateGroupSaga), @@ -414,8 +362,5 @@ export default function* orgSagas() { takeLatest(ReduxActionTypes.UPDATE_ORG, updateOrgSaga), takeLatest(ReduxActionTypes.FETCH_ORG_API_USAGE, fetchAPIUsageSaga), takeLatest(ReduxActionTypes.FETCH_ORG_LAST_MONTH_API_USAGE, fetchLastMonthAPIUsageSaga), - takeLatest(ReduxActionTypes.FETCH_WORKSPACES_INIT, fetchWorkspacesSaga), - - ]); } diff --git a/client/packages/lowcoder/src/redux/sagas/userSagas.ts b/client/packages/lowcoder/src/redux/sagas/userSagas.ts index b19e2b1a65..1e66f73095 100644 --- a/client/packages/lowcoder/src/redux/sagas/userSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/userSagas.ts @@ -25,7 +25,6 @@ import { messageInstance } from "lowcoder-design/src/components/GlobalInstances" import { AuthSearchParams } from "constants/authConstants"; import { saveAuthSearchParams } from "pages/userAuth/authUtils"; import { initTranslator } from "i18n"; -import { fetchWorkspacesAction } from "../reduxActions/orgActions"; function validResponseData(response: AxiosResponse) { return response && response.data && response.data.data; diff --git a/client/packages/lowcoder/src/redux/selectors/orgSelectors.ts b/client/packages/lowcoder/src/redux/selectors/orgSelectors.ts index 322f414f7b..99a7908a3b 100644 --- a/client/packages/lowcoder/src/redux/selectors/orgSelectors.ts +++ b/client/packages/lowcoder/src/redux/selectors/orgSelectors.ts @@ -1,5 +1,4 @@ import { Org } from "@lowcoder-ee/constants/orgConstants"; -import { getUser } from "./usersSelectors"; import { AppState } from "redux/reducers"; import { getHomeOrg } from "./applicationSelector"; @@ -31,9 +30,6 @@ export const getOrgLastMonthApiUsage = (state: AppState) => { return state.ui.org.lastMonthApiUsage; } -// Add to usersSelectors.ts -export const getWorkspaces = (state: AppState) => state.ui.users.workspaces; - export const getCurrentOrg = (state: AppState): Pick | undefined => { const homeOrg = getHomeOrg(state); if (!homeOrg) { diff --git a/client/packages/lowcoder/src/util/useWorkspaceManager.ts b/client/packages/lowcoder/src/util/useWorkspaceManager.ts index fe2379769d..3383133489 100644 --- a/client/packages/lowcoder/src/util/useWorkspaceManager.ts +++ b/client/packages/lowcoder/src/util/useWorkspaceManager.ts @@ -1,201 +1,93 @@ -import { useReducer, useEffect, useCallback, useMemo } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useState, useEffect, useCallback, useMemo } from 'react'; 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 -interface WorkspaceState { - searchTerm: string; - currentPage: number; - currentPageWorkspaces: Org[]; - totalCount: number; - isLoading: boolean; -} - -// Action types for the reducer -type WorkspaceAction = - | { type: 'SET_SEARCH_TERM'; payload: string } - | { type: 'SET_PAGE'; payload: number } - | { type: 'SET_LOADING'; payload: boolean } - | { type: 'SET_WORKSPACES'; payload: { workspaces: Org[]; total: number } } - | { type: 'RESET'; payload: { totalCount: number } }; - -// Initial state -const initialState: WorkspaceState = { - searchTerm: '', - currentPage: 1, - currentPageWorkspaces: [], - totalCount: 0, - isLoading: false, -}; - -// Reducer function - handles state transitions -function workspaceReducer(state: WorkspaceState, action: WorkspaceAction): WorkspaceState { - switch (action.type) { - case 'SET_SEARCH_TERM': - return { - ...state, - searchTerm: action.payload, - currentPage: 1 , - isLoading: Boolean(action.payload.trim()) - }; - case 'SET_PAGE': - return { ...state, currentPage: action.payload }; - case 'SET_LOADING': - return { ...state, isLoading: action.payload }; - case 'SET_WORKSPACES': - return { - ...state, - currentPageWorkspaces: action.payload.workspaces, - totalCount: action.payload.total, - isLoading: false, - }; - case 'RESET': - return { - ...initialState, - totalCount: action.payload.totalCount, - }; - default: - return state; - } -} - -// Hook interface interface UseWorkspaceManagerOptions { pageSize?: number; } -// Main hook export function useWorkspaceManager({ pageSize = 10 }: UseWorkspaceManagerOptions) { - // Get workspaces from Redux - const workspaces = useSelector(getWorkspaces); - const reduxDispatch = useDispatch(); - - // Initialize reducer with Redux total count - const [state, dispatch] = useReducer(workspaceReducer, { - ...initialState, - totalCount: workspaces.totalCount, - }); - - - - /* ----- first-time fetch ------------------------------------------------ */ - useEffect(() => { - if (!workspaces.isFetched && !workspaces.loading) { - reduxDispatch(fetchWorkspacesAction(1, pageSize)); - } - }, [workspaces.isFetched, workspaces.loading, pageSize, reduxDispatch]); + const [searchTerm, setSearchTerm] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [workspaces, setWorkspaces] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); - // API call to fetch workspaces (memoized for stable reference) - const fetchWorkspacesPage = useCallback( + const fetchWorkspaces = useCallback( async (page: number, search?: string) => { - dispatch({ type: 'SET_LOADING', payload: true }); - + setIsLoading(true); try { const response = await UserApi.getMyOrgs(page, pageSize, search); if (response.data.success) { const apiData = response.data.data; - const transformedItems = apiData.data.map(item => ({ - id: item.orgView.orgId, - name: item.orgView.orgName, - createdAt: item.orgView.createdAt, - updatedAt: item.orgView.updatedAt, - isCurrentOrg: item.isCurrentOrg, - })); - - dispatch({ - type: 'SET_WORKSPACES', - payload: { - workspaces: transformedItems as Org[], - total: apiData.total, - }, - }); + const items = apiData.data + .filter(item => item.orgView && item.orgView.orgId) + .map(item => ({ + id: item.orgView.orgId, + name: item.orgView.orgName, + createdAt: item.orgView.createdAt, + updatedAt: item.orgView.updatedAt, + isCurrentOrg: item.isCurrentOrg, + })) as Org[]; + + setWorkspaces(items); + setTotalCount(apiData.total); } } catch (error) { console.error('Error fetching workspaces:', error); - dispatch({ type: 'SET_WORKSPACES', payload: { workspaces: [], total: 0 } }); + setWorkspaces([]); + setTotalCount(0); + } finally { + setIsLoading(false); } }, - [dispatch, pageSize] + [pageSize] ); - // Debounced search function (memoized to keep a single instance across renders) - const debouncedSearch = useMemo(() => - debounce(async (term: string) => { - if (!term.trim()) { - // Clear search - reset to Redux data - dispatch({ - type: 'SET_WORKSPACES', - payload: { workspaces: [], total: workspaces.totalCount }, - }); - return; - } - - // Perform search - await fetchWorkspacesPage(1, term); - }, 500) - , [dispatch, fetchWorkspacesPage, workspaces.totalCount]); + // Initial fetch + useEffect(() => { + fetchWorkspaces(1); + }, [fetchWorkspaces]); + + const debouncedSearch = useMemo( + () => debounce((term: string) => { + fetchWorkspaces(1, term || undefined); + }, 500), + [fetchWorkspaces] + ); - // Cleanup debounce on unmount useEffect(() => { - return () => { - debouncedSearch.cancel(); - }; + return () => { debouncedSearch.cancel(); }; }, [debouncedSearch]); - // Handle search input change const handleSearchChange = (value: string) => { - dispatch({ type: 'SET_SEARCH_TERM', payload: value }); + setSearchTerm(value); + setCurrentPage(1); debouncedSearch(value); }; - // Handle page change const handlePageChange = (page: number) => { - dispatch({ type: 'SET_PAGE', payload: page }); - - if (page === 1 && !state.searchTerm.trim()) { - // Page 1 + no search = use Redux data - dispatch({ - type: 'SET_WORKSPACES', - payload: { workspaces: [], total: workspaces.totalCount } - }); - } else { - // Other pages or search = fetch from API - fetchWorkspacesPage(page, state.searchTerm.trim() || undefined); - } + setCurrentPage(page); + fetchWorkspaces(page, searchTerm.trim() || undefined); }; - // Determine which workspaces to display - const displayWorkspaces = (() => { - if (state.searchTerm.trim() || state.currentPage > 1) { - return state.currentPageWorkspaces; // API results - } - return workspaces.items; // Redux data for page 1 - })(); - - // Determine current total count - const currentTotalCount = state.searchTerm.trim() - ? state.totalCount - : workspaces.totalCount; + const refetch = useCallback( + () => fetchWorkspaces(currentPage, searchTerm.trim() || undefined), + [fetchWorkspaces, currentPage, searchTerm] + ); return { - // State - searchTerm: state.searchTerm, - currentPage: state.currentPage, - isLoading: state.isLoading || workspaces.loading, - displayWorkspaces, - totalCount: currentTotalCount, - - // Actions + searchTerm, + currentPage, + isLoading, + displayWorkspaces: workspaces, + totalCount, handleSearchChange, handlePageChange, - - // Config pageSize, + refetch, }; -} \ No newline at end of file +}