From f80d8bcb15e16c7288b7e2e1060624ad7de4679d Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Mon, 9 Sep 2024 12:17:49 +0200 Subject: [PATCH 1/9] Visual Control for concurrent App Edit --- .../lowcoder/src/pages/common/header.tsx | 58 +++++++++++++++---- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/header.tsx b/client/packages/lowcoder/src/pages/common/header.tsx index b37178659..d10138492 100644 --- a/client/packages/lowcoder/src/pages/common/header.tsx +++ b/client/packages/lowcoder/src/pages/common/header.tsx @@ -54,6 +54,9 @@ import { getBrandingConfig } from "../../redux/selectors/configSelectors"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { EditorContext } from "../../comps/editorState"; import Tooltip from "antd/es/tooltip"; +import { LockOutlined } from '@ant-design/icons'; +import Avatar from 'antd/es/avatar'; + const StyledLink = styled.a` display: flex; @@ -234,12 +237,6 @@ const DropdownStyled = styled(Dropdown)` } `; -const DropdownMenuStyled = styled(DropdownMenu)` - .ant-dropdown-menu-item:hover { - background: #edf4fa; - } -`; - const Wrapper = styled.div` .taco-edit-text-wrapper { width: fit-content; @@ -262,6 +259,16 @@ const Prefix = styled.div` } `; +// Add the lock icon logic for disabled options +const DropdownMenuStyled = styled(DropdownMenu)` + .ant-dropdown-menu-item:hover { + background: ${(props) => + props.disabled ? 'inherit' : '#edf4fa'}; + cursor: ${(props) => + props.disabled ? 'not-allowed' : 'pointer'}; + } +`; + function HeaderProfile(props: { user: User }) { const { user } = props; const fetchingUser = useSelector(isFetchingUser); @@ -318,6 +325,10 @@ export default function Header(props: HeaderProps) { const isModule = appType === AppTypeEnum.Module; + // Raheel: Todo - get concurrent editing state by API + // maybe via editorState.getConcurrentAppEditingState(); as a new function? + const [concurrentAppEditingState, setConcurrentAppEditingState] = useState(true); + const editorModeOptions = [ { label: trans("header.editorMode_layout"), @@ -458,6 +469,16 @@ export default function Header(props: HeaderProps) { ) : ( <> + {/* Display a hint about who is editing the app */} + {concurrentAppEditingState && ( +
+ + + {`${user.username} is currently editing this app.`} + +
+ )} + {applicationId && ( )} + preview(applicationId)}> {trans("header.preview")} - + { + if (concurrentAppEditingState) return; // Prevent clicks if the app is being edited by someone else if (e.key === "deploy") { dispatch(publishApplication({ applicationId })); } else if (e.key === "snapshot") { @@ -494,24 +517,36 @@ export default function Header(props: HeaderProps) { { key: "deploy", label: ( - {trans("header.deploy")} +
+ {concurrentAppEditingState && } + + {trans("header.deploy")} + +
), + disabled: concurrentAppEditingState, }, { key: "snapshot", label: ( - {trans("header.snapshot")} +
+ {concurrentAppEditingState && } + + {trans("header.snapshot")} + +
), + disabled: concurrentAppEditingState, }, ]} /> )} > - +
- + ); @@ -520,6 +555,7 @@ export default function Header(props: HeaderProps) { showAppSnapshot, applicationId, permissionDialogVisible, + concurrentAppEditingState, // Include the state in the dependency array ]); return ( From 4ac37a824503a2bb2a5ec322cfa2466754831f05 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Mon, 9 Sep 2024 12:28:53 +0200 Subject: [PATCH 2/9] Visual Control for concurrent App Edit 2 --- .../lowcoder/src/pages/common/header.tsx | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/header.tsx b/client/packages/lowcoder/src/pages/common/header.tsx index d10138492..f84d7bfdc 100644 --- a/client/packages/lowcoder/src/pages/common/header.tsx +++ b/client/packages/lowcoder/src/pages/common/header.tsx @@ -54,7 +54,7 @@ import { getBrandingConfig } from "../../redux/selectors/configSelectors"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; import { EditorContext } from "../../comps/editorState"; import Tooltip from "antd/es/tooltip"; -import { LockOutlined } from '@ant-design/icons'; +import { LockOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; import Avatar from 'antd/es/avatar'; @@ -259,6 +259,27 @@ const Prefix = styled.div` } `; +const EditingNoticeWrapper = styled.div` + display: flex; + align-items: center; + background-color: #ffe6e6; /* Light red background for warning */ + padding: 2px 8px; + border-radius: 5px; + margin-right: 8px; +`; + +const EditingHintText = styled.span` + margin-left: 8px; + font-size: 12px; + color: #ff4d4f; /* Red color to indicate warning */ +`; + +const WarningIcon = styled(ExclamationCircleOutlined)` + margin-left: 8px; + font-size: 16px; + color: #ff4d4f; /* Red color for the icon */ +`; + // Add the lock icon logic for disabled options const DropdownMenuStyled = styled(DropdownMenu)` .ant-dropdown-menu-item:hover { @@ -471,12 +492,19 @@ export default function Header(props: HeaderProps) { <> {/* Display a hint about who is editing the app */} {concurrentAppEditingState && ( -
- - - {`${user.username} is currently editing this app.`} - -
+ + + + + {`${user.username} is currently editing this app.`} + + + + )} {applicationId && ( From 8470a4226957c292cc7eb51d67d52830a27127b1 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Mon, 16 Sep 2024 13:08:48 +0500 Subject: [PATCH 3/9] Block editing if someone else is editing the app --- .../lowcoder/src/api/applicationApi.ts | 10 +++- .../src/constants/applicationConstants.ts | 1 + .../src/constants/reduxActionConstants.ts | 1 + .../lowcoder/src/pages/common/header.tsx | 48 +++++++++++++------ .../redux/reduxActions/applicationActions.ts | 11 +++++ .../src/redux/sagas/applicationSagas.ts | 13 +++++ 6 files changed, 68 insertions(+), 16 deletions(-) diff --git a/client/packages/lowcoder/src/api/applicationApi.ts b/client/packages/lowcoder/src/api/applicationApi.ts index 827df5032..d38ee1843 100644 --- a/client/packages/lowcoder/src/api/applicationApi.ts +++ b/client/packages/lowcoder/src/api/applicationApi.ts @@ -9,6 +9,7 @@ import { PublishApplicationPayload, RecycleApplicationPayload, RestoreApplicationPayload, + SetAppEditingStatePayload, UpdateAppPermissionPayload, } from "redux/reduxActions/applicationActions"; import { ApiResponse, GenericApiResponse } from "./apiResponses"; @@ -96,7 +97,7 @@ class ApplicationApi extends Api { static publicToAllURL = (applicationId: string) => `/applications/${applicationId}/public-to-all`; static publicToMarketplaceURL = (applicationId: string) => `/applications/${applicationId}/public-to-marketplace`; static getMarketplaceAppURL = (applicationId: string) => `/applications/${applicationId}/view_marketplace`; - + static setAppEditingStateURL = (applicationId: string) => `/applications/editState/${applicationId}`; static fetchHomeData(request: HomeDataPayload): AxiosPromise { return Api.get(ApplicationApi.fetchHomeDataURL, request); @@ -232,6 +233,13 @@ class ApplicationApi extends Api { static getMarketplaceApp(appId: string) { return Api.get(ApplicationApi.getMarketplaceAppURL(appId)); } + + static setAppEditingState(request: SetAppEditingStatePayload): AxiosPromise { + const { applicationId, editingFinished } = request; + return Api.put(ApplicationApi.setAppEditingStateURL(applicationId), { + editingFinished, + }); + } } export default ApplicationApi; diff --git a/client/packages/lowcoder/src/constants/applicationConstants.ts b/client/packages/lowcoder/src/constants/applicationConstants.ts index 711bee4e6..c6e277796 100644 --- a/client/packages/lowcoder/src/constants/applicationConstants.ts +++ b/client/packages/lowcoder/src/constants/applicationConstants.ts @@ -91,6 +91,7 @@ export interface ApplicationMeta { folder: false; isLocalMarketplace?: boolean; applicationStatus: "NORMAL" | "RECYCLED" | "DELETED"; + editingUserId: string | null; } export interface FolderMeta { diff --git a/client/packages/lowcoder/src/constants/reduxActionConstants.ts b/client/packages/lowcoder/src/constants/reduxActionConstants.ts index 9a3a49886..66178633c 100644 --- a/client/packages/lowcoder/src/constants/reduxActionConstants.ts +++ b/client/packages/lowcoder/src/constants/reduxActionConstants.ts @@ -144,6 +144,7 @@ export const ReduxActionTypes = { FETCH_ALL_MODULES_SUCCESS: "FETCH_ALL_MODULES_SUCCESS", FETCH_ALL_MARKETPLACE_APPS: "FETCH_ALL_MARKETPLACE_APPS", FETCH_ALL_MARKETPLACE_APPS_SUCCESS: "FETCH_ALL_MARKETPLACE_APPS_SUCCESS", + SET_APP_EDITING_STATE: "SET_APP_EDITING_STATE", /* user profile */ SET_USER_PROFILE_SETTING_MODAL_VISIBLE: "SET_USER_PROFILE_SETTING_MODAL_VISIBLE", diff --git a/client/packages/lowcoder/src/pages/common/header.tsx b/client/packages/lowcoder/src/pages/common/header.tsx index f84d7bfdc..73e3dba3c 100644 --- a/client/packages/lowcoder/src/pages/common/header.tsx +++ b/client/packages/lowcoder/src/pages/common/header.tsx @@ -9,7 +9,7 @@ import { AUTH_LOGIN_URL, preview, } from "constants/routesURL"; -import { User } from "constants/userConstants"; +import { CurrentUser, User } from "constants/userConstants"; import { CommonTextLabel, CustomModal, @@ -56,6 +56,8 @@ import { EditorContext } from "../../comps/editorState"; import Tooltip from "antd/es/tooltip"; import { LockOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; import Avatar from 'antd/es/avatar'; +import UserApi from "@lowcoder-ee/api/userApi"; +import { validateResponse } from "@lowcoder-ee/api/apiUtils"; const StyledLink = styled.a` @@ -343,12 +345,26 @@ export default function Header(props: HeaderProps) { const [editName, setEditName] = useState(false); const [editing, setEditing] = useState(false); const [permissionDialogVisible, setPermissionDialogVisible] = useState(false); + const [editingUser, setEditingUser] = useState(); const isModule = appType === AppTypeEnum.Module; + const blockEditing = useMemo( + () => user.id !== application?.editingUserId, + [application?.editingUserId] + ); - // Raheel: Todo - get concurrent editing state by API - // maybe via editorState.getConcurrentAppEditingState(); as a new function? - const [concurrentAppEditingState, setConcurrentAppEditingState] = useState(true); + useEffect(() => { + if(blockEditing && application && Boolean(application?.editingUserId)) { + UserApi.getUserDetail(application.editingUserId!) + .then(resp => { + if (validateResponse(resp)) { + console.log(resp.data.data); + setEditingUser(resp.data.data); + } + }); + } + }, [blockEditing]); + console.log(user.id, application?.editingUserId); const editorModeOptions = [ { @@ -491,7 +507,7 @@ export default function Header(props: HeaderProps) { ) : ( <> {/* Display a hint about who is editing the app */} - {concurrentAppEditingState && ( + {blockEditing && ( - {`${user.username} is currently editing this app.`} + {/* {`${user.username} is currently editing this app.`} */} + {`${editingUser?.name || 'Someone'} is currently editing this app`} @@ -534,7 +551,7 @@ export default function Header(props: HeaderProps) { { - if (concurrentAppEditingState) return; // Prevent clicks if the app is being edited by someone else + if (blockEditing) return; // Prevent clicks if the app is being edited by someone else if (e.key === "deploy") { dispatch(publishApplication({ applicationId })); } else if (e.key === "snapshot") { @@ -546,31 +563,31 @@ export default function Header(props: HeaderProps) { key: "deploy", label: (
- {concurrentAppEditingState && } - + {blockEditing && } + {trans("header.deploy")}
), - disabled: concurrentAppEditingState, + disabled: blockEditing, }, { key: "snapshot", label: (
- {concurrentAppEditingState && } - + {blockEditing && } + {trans("header.snapshot")}
), - disabled: concurrentAppEditingState, + disabled: blockEditing, }, ]} /> )} > - + @@ -583,7 +600,8 @@ export default function Header(props: HeaderProps) { showAppSnapshot, applicationId, permissionDialogVisible, - concurrentAppEditingState, // Include the state in the dependency array + blockEditing, // Include the state in the dependency array + editingUser?.name, ]); return ( diff --git a/client/packages/lowcoder/src/redux/reduxActions/applicationActions.ts b/client/packages/lowcoder/src/redux/reduxActions/applicationActions.ts index 15a69d7fe..6d3a0310c 100644 --- a/client/packages/lowcoder/src/redux/reduxActions/applicationActions.ts +++ b/client/packages/lowcoder/src/redux/reduxActions/applicationActions.ts @@ -134,6 +134,12 @@ export type FetchAppInfoPayload = { onSuccess?: (info: AppSummaryInfo) => void; onError?: (error: string) => void; }; + +export type SetAppEditingStatePayload = { + applicationId: string; + editingFinished: boolean; +}; + export const fetchApplicationInfo = (payload: FetchAppInfoPayload) => ({ type: ReduxActionTypes.FETCH_APPLICATION_DETAIL, payload: payload, @@ -170,3 +176,8 @@ export const deleteAppPermission = (payload: DeleteAppPermissionPayload) => ({ type: ReduxActionTypes.DELETE_APP_PERMISSION, payload: payload, }); + +export const setAppEditingState = (payload: SetAppEditingStatePayload) => ({ + type: ReduxActionTypes.SET_APP_EDITING_STATE, + payload: payload, +}); diff --git a/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts b/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts index d5d1b5a31..b6299d626 100644 --- a/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts +++ b/client/packages/lowcoder/src/redux/sagas/applicationSagas.ts @@ -22,6 +22,7 @@ import { PublishApplicationPayload, RecycleApplicationPayload, RestoreApplicationPayload, + SetAppEditingStatePayload, UpdateApplicationPayload, UpdateAppMetaPayload, UpdateAppPermissionPayload, @@ -391,6 +392,17 @@ function* fetchAllMarketplaceAppsSaga() { } } +function* setAppEditingStateSaga(action: ReduxAction) { + try { + yield call( + ApplicationApi.setAppEditingState, + action.payload + ); + } catch (error) { + log.debug("set app editing state: ", error); + } +} + export default function* applicationSagas() { yield all([ takeLatest(ReduxActionTypes.FETCH_HOME_DATA, fetchHomeDataSaga), @@ -416,5 +428,6 @@ export default function* applicationSagas() { ReduxActionTypes.FETCH_ALL_MARKETPLACE_APPS, fetchAllMarketplaceAppsSaga, ), + takeLatest(ReduxActionTypes.SET_APP_EDITING_STATE, setAppEditingStateSaga), ]); } From 737c25cc3696b00145ff99ab71f5bff090cef336 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Wed, 18 Sep 2024 23:52:23 +0500 Subject: [PATCH 4/9] logic to block editing if someone else is editing --- .../src/constants/applicationConstants.ts | 1 + .../lowcoder/src/pages/common/header.tsx | 7 +--- .../lowcoder/src/pages/editor/AppEditor.tsx | 41 +++++++++++++++++-- .../src/pages/editor/appEditorInternal.tsx | 22 +++++++--- .../src/pages/editor/useRootCompInstance.tsx | 9 +++- .../src/util/context/ExternalEditorContext.ts | 5 +++ .../lowcoder/src/util/editoryHistory.ts | 9 +++- 7 files changed, 75 insertions(+), 19 deletions(-) diff --git a/client/packages/lowcoder/src/constants/applicationConstants.ts b/client/packages/lowcoder/src/constants/applicationConstants.ts index c6e277796..22a134da6 100644 --- a/client/packages/lowcoder/src/constants/applicationConstants.ts +++ b/client/packages/lowcoder/src/constants/applicationConstants.ts @@ -92,6 +92,7 @@ export interface ApplicationMeta { isLocalMarketplace?: boolean; applicationStatus: "NORMAL" | "RECYCLED" | "DELETED"; editingUserId: string | null; + lastEditedAt: number; } export interface FolderMeta { diff --git a/client/packages/lowcoder/src/pages/common/header.tsx b/client/packages/lowcoder/src/pages/common/header.tsx index 73e3dba3c..06cbf8b2c 100644 --- a/client/packages/lowcoder/src/pages/common/header.tsx +++ b/client/packages/lowcoder/src/pages/common/header.tsx @@ -332,6 +332,7 @@ type HeaderProps = { // header in editor page export default function Header(props: HeaderProps) { const editorState = useContext(EditorContext); + const { blockEditing } = useContext(ExternalEditorContext); const { togglePanel } = props; const { toggleEditorModeStatus } = props; const { left, bottom, right } = props.panelStatus; @@ -348,23 +349,17 @@ export default function Header(props: HeaderProps) { const [editingUser, setEditingUser] = useState(); const isModule = appType === AppTypeEnum.Module; - const blockEditing = useMemo( - () => user.id !== application?.editingUserId, - [application?.editingUserId] - ); useEffect(() => { if(blockEditing && application && Boolean(application?.editingUserId)) { UserApi.getUserDetail(application.editingUserId!) .then(resp => { if (validateResponse(resp)) { - console.log(resp.data.data); setEditingUser(resp.data.data); } }); } }, [blockEditing]); - console.log(user.id, application?.editingUserId); const editorModeOptions = [ { diff --git a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx index b11a4fce0..6565cbb9c 100644 --- a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx +++ b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx @@ -32,6 +32,8 @@ import { ALL_APPLICATIONS_URL } from "@lowcoder-ee/constants/routesURL"; import history from "util/history"; import Flex from "antd/es/flex"; import React from "react"; +import dayjs from "dayjs"; +import { currentApplication } from "@lowcoder-ee/redux/selectors/applicationSelector"; const AppSnapshot = lazy(() => { return import("pages/editor/appSnapshot") @@ -52,13 +54,16 @@ const AppEditor = React.memo(() => { const paramViewMode = params.viewMode || window.location.pathname.split("/")[3]; const viewMode = (paramViewMode === "view" || paramViewMode === "admin") ? "published" : paramViewMode === "view_marketplace" ? "view_marketplace" : "editing"; const currentUser = useSelector(getUser); + const application = useSelector(currentApplication); const dispatch = useDispatch(); const fetchOrgGroupsFinished = useSelector(getFetchOrgGroupsFinished); const isCommonSettingsFetching = useSelector(getIsCommonSettingFetching); const orgId = currentUser.currentOrgId; const firstRendered = useRef(false); + const fetchInterval = useRef(0); const [isDataSourcePluginRegistered, setIsDataSourcePluginRegistered] = useState(false); const [appError, setAppError] = useState(''); + const [blockEditing, setBlockEditing] = useState(false); setGlobalSettings({ applicationId, isViewMode: paramViewMode === "view" }); @@ -83,7 +88,22 @@ const AppEditor = React.memo(() => { }); const readOnly = isUserViewMode; - const compInstance = useRootCompInstance(appInfo, readOnly, isDataSourcePluginRegistered); + const compInstance = useRootCompInstance( + appInfo, + readOnly, + isDataSourcePluginRegistered, + blockEditing, + ); + + useEffect(() => { + if (currentUser && application) { + const lastEditedAt = dayjs(application?.lastEditedAt); + const lastEditedDiff = dayjs().diff(lastEditedAt, 'minutes'); + const shouldBlockEditing = currentUser.id !== application?.createBy && lastEditedDiff < 5; + setBlockEditing(shouldBlockEditing); + console.log('blockEditing', shouldBlockEditing, {user_id: currentUser.id, editingUserId: application.createBy, lastEditedDiff}); + } + }, [application, currentUser]); // fetch dataSource and plugin useEffect(() => { @@ -100,7 +120,7 @@ const AppEditor = React.memo(() => { dispatch(fetchQueryLibraryDropdown()); } }, [dispatch, applicationId, paramViewMode]); - + const fetchJSDataSourceByApp = () => { DatasourceApi.fetchJsDatasourceByApp(applicationId).then((res) => { res.data.data.forEach((i) => { @@ -117,7 +137,7 @@ const AppEditor = React.memo(() => { } }, [dispatch, fetchOrgGroupsFinished, orgId]); - useEffect(() => { + const fetchApplication = useCallback(() => { dispatch( fetchApplicationInfo({ type: viewMode, @@ -142,6 +162,20 @@ const AppEditor = React.memo(() => { ); }, [viewMode, applicationId, dispatch]); + useEffect(() => { + fetchApplication(); + }, [fetchApplication]); + + // useEffect(() => { + // if(!blockEditing) return clearInterval(fetchInterval.current); + // if(blockEditing) { + // fetchInterval.current = window.setInterval(() => { + // fetchApplication(); + // }, 60000); + // } + // return () => clearInterval(fetchInterval.current); + // }, [blockEditing, fetchApplication]); + const fallbackUI = useMemo(() => ( { (); useEffect(() => { - if (readOnly) { + if (readOnly || blockEditing) { return; } if (!comp || comp === prevComp) { @@ -74,6 +75,7 @@ function useSaveComp( interface AppEditorInternalViewProps { readOnly: boolean; + blockEditing?: boolean; appInfo: AppSummaryInfo; loading: boolean; compInstance: RootCompInstanceType; @@ -83,7 +85,7 @@ export const AppEditorInternalView = React.memo((props: AppEditorInternalViewPro const isUserViewMode = useUserViewMode(); const extraExternalEditorState = useSelector(getExternalEditorState); const dispatch = useDispatch(); - const { readOnly, appInfo, compInstance } = props; + const { readOnly, blockEditing, appInfo, compInstance } = props; const [externalEditorState, setExternalEditorState] = useState({ changeExternalState: (state: Partial) => { @@ -92,8 +94,7 @@ export const AppEditorInternalView = React.memo((props: AppEditorInternalViewPro applicationId: appInfo.id, appType: AppTypeEnum.Application, }); - useSaveComp(appInfo.id, readOnly, compInstance); - + useEffect(() => { setExternalEditorState((s) => ({ ...s, @@ -102,9 +103,16 @@ export const AppEditorInternalView = React.memo((props: AppEditorInternalViewPro appType: appInfo.appType, applicationId: appInfo.id, hideHeader: window.location.pathname.split("/")[3] === "admin", + blockEditing, ...extraExternalEditorState, })); - }, [compInstance?.history, extraExternalEditorState, readOnly, appInfo.appType, appInfo.id]); + }, [ + compInstance?.history, + extraExternalEditorState, + readOnly, + appInfo.appType, appInfo.id, + blockEditing, + ]); useEffect(() => { message.config({ @@ -112,6 +120,8 @@ export const AppEditorInternalView = React.memo((props: AppEditorInternalViewPro }); }, [isUserViewMode]); + useSaveComp(appInfo.id, readOnly, compInstance, blockEditing); + const loading = !compInstance || !compInstance.comp || !compInstance.comp.preloaded || props.loading; diff --git a/client/packages/lowcoder/src/pages/editor/useRootCompInstance.tsx b/client/packages/lowcoder/src/pages/editor/useRootCompInstance.tsx index a289d3321..15411f8c8 100644 --- a/client/packages/lowcoder/src/pages/editor/useRootCompInstance.tsx +++ b/client/packages/lowcoder/src/pages/editor/useRootCompInstance.tsx @@ -7,7 +7,12 @@ import { useCompInstance } from "comps/utils/useCompInstance"; import { MarkAppInitialized, perfMark } from "util/perfUtils"; import { QueryApi } from "api/queryApi"; -export function useRootCompInstance(appInfo: AppSummaryInfo, readOnly: boolean, isReady: boolean) { +export function useRootCompInstance( + appInfo: AppSummaryInfo, + readOnly: boolean, + isReady: boolean, + blockEditing?: boolean, +) { const appId = appInfo.id; const params = useMemo(() => { return { @@ -28,7 +33,7 @@ export function useRootCompInstance(appInfo: AppSummaryInfo, readOnly: boolean, }; }, [appId, appInfo.dsl, appInfo.moduleDsl, isReady, readOnly]); const [comp, container] = useCompInstance(params); - const history = useAppHistory(container, readOnly, appId); + const history = useAppHistory(container, readOnly, appId, blockEditing); useUnmount(() => { comp?.clearPreload(); diff --git a/client/packages/lowcoder/src/util/context/ExternalEditorContext.ts b/client/packages/lowcoder/src/util/context/ExternalEditorContext.ts index 21d1bf482..f1c64f8a6 100644 --- a/client/packages/lowcoder/src/util/context/ExternalEditorContext.ts +++ b/client/packages/lowcoder/src/util/context/ExternalEditorContext.ts @@ -38,6 +38,11 @@ export interface ExternalEditorContextState { */ showScriptsAndStyleModal?: boolean; + /** + * whether to block editing if someone else is editing the app + */ + blockEditing?: boolean; + changeExternalState?: (state: Partial) => void; } diff --git a/client/packages/lowcoder/src/util/editoryHistory.ts b/client/packages/lowcoder/src/util/editoryHistory.ts index 421871c52..24544f8fc 100644 --- a/client/packages/lowcoder/src/util/editoryHistory.ts +++ b/client/packages/lowcoder/src/util/editoryHistory.ts @@ -12,7 +12,12 @@ import { showCost } from "util/perfUtils"; type OperationType = AppSnapshotContext["operations"]; -export function useAppHistory(compContainer: CompContainer, readOnly: boolean, appId: string) { +export function useAppHistory( + compContainer: CompContainer, + readOnly: boolean, + appId: string, + blockEditing?: boolean, +) { const reduxDispatch = useDispatch(); return useMemo(() => { @@ -32,7 +37,7 @@ export function useAppHistory(compContainer: CompContainer, readOnly: boolean, a }; compContainer.addChangeListener((actions) => { - if (readOnly || !actions) { + if (readOnly || !actions || blockEditing) { return; } // maybe slow: comparing dsl by `toJson` From 5f6c18fab1215ca374cbea9bc97a8f38474d2c74 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Thu, 19 Sep 2024 14:26:59 +0500 Subject: [PATCH 5/9] added memoization in appEditor --- .../lowcoder/src/pages/editor/AppEditor.tsx | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx index 6565cbb9c..77de27704 100644 --- a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx +++ b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx @@ -46,21 +46,37 @@ const AppEditorInternalView = lazy( ); const AppEditor = React.memo(() => { - const showAppSnapshot = useSelector(showAppSnapshotSelector); + const dispatch = useDispatch(); const params = useParams(); const isUserViewModeCheck = useUserViewMode(); - const isUserViewMode = params.viewMode ? isUserViewModeCheck : true; - const applicationId = params.applicationId || window.location.pathname.split("/")[2]; - const paramViewMode = params.viewMode || window.location.pathname.split("/")[3]; - const viewMode = (paramViewMode === "view" || paramViewMode === "admin") ? "published" : paramViewMode === "view_marketplace" ? "view_marketplace" : "editing"; + const showAppSnapshot = useSelector(showAppSnapshotSelector); const currentUser = useSelector(getUser); - const application = useSelector(currentApplication); - const dispatch = useDispatch(); const fetchOrgGroupsFinished = useSelector(getFetchOrgGroupsFinished); const isCommonSettingsFetching = useSelector(getIsCommonSettingFetching); - const orgId = currentUser.currentOrgId; + const application = useSelector(currentApplication); + + const isUserViewMode = useMemo( + () => params.viewMode ? isUserViewModeCheck : true, + [params.viewMode, isUserViewModeCheck] + ); + const applicationId = useMemo( + () => params.applicationId || window.location.pathname.split("/")[2], + [params.applicationId, window.location.pathname] + ); + const paramViewMode = useMemo( + () => params.viewMode || window.location.pathname.split("/")[3], + [params.viewMode, window.location.pathname] + ); + const viewMode = useMemo( + () => (paramViewMode === "view" || paramViewMode === "admin") + ? "published" + : paramViewMode === "view_marketplace" ? "view_marketplace" : "editing", + [paramViewMode] + ); + const firstRendered = useRef(false); const fetchInterval = useRef(0); + const orgId = useMemo(() => currentUser.currentOrgId, [currentUser.currentOrgId]); const [isDataSourcePluginRegistered, setIsDataSourcePluginRegistered] = useState(false); const [appError, setAppError] = useState(''); const [blockEditing, setBlockEditing] = useState(false); @@ -99,7 +115,8 @@ const AppEditor = React.memo(() => { if (currentUser && application) { const lastEditedAt = dayjs(application?.lastEditedAt); const lastEditedDiff = dayjs().diff(lastEditedAt, 'minutes'); - const shouldBlockEditing = currentUser.id !== application?.createBy && lastEditedDiff < 5; + // const shouldBlockEditing = currentUser.id !== application?.createBy && lastEditedDiff < 5; + const shouldBlockEditing = lastEditedDiff < 5; setBlockEditing(shouldBlockEditing); console.log('blockEditing', shouldBlockEditing, {user_id: currentUser.id, editingUserId: application.createBy, lastEditedDiff}); } @@ -120,8 +137,8 @@ const AppEditor = React.memo(() => { dispatch(fetchQueryLibraryDropdown()); } }, [dispatch, applicationId, paramViewMode]); - - const fetchJSDataSourceByApp = () => { + + const fetchJSDataSourceByApp = useCallback(() => { DatasourceApi.fetchJsDatasourceByApp(applicationId).then((res) => { res.data.data.forEach((i) => { registryDataSourcePlugin(i.type, i.id, i.pluginDefinition); @@ -129,7 +146,13 @@ const AppEditor = React.memo(() => { setIsDataSourcePluginRegistered(true); }); dispatch(setShowAppSnapshot(false)); - }; + }, [ + applicationId, + registryDataSourcePlugin, + setIsDataSourcePluginRegistered, + setShowAppSnapshot, + dispatch, + ]); useEffect(() => { if (!fetchOrgGroupsFinished) { @@ -160,7 +183,7 @@ const AppEditor = React.memo(() => { } }) ); - }, [viewMode, applicationId, dispatch]); + }, [viewMode, applicationId, dispatch, fetchJSDataSourceByApp]); useEffect(() => { fetchApplication(); From a6f4bd43a922852a8410dd2738ae048d6ca72661 Mon Sep 17 00:00:00 2001 From: RAHEEL Date: Thu, 19 Sep 2024 16:00:57 +0500 Subject: [PATCH 6/9] logic to wait for editing enabled --- .../lowcoder/src/pages/editor/AppEditor.tsx | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx index 77de27704..fee04fda9 100644 --- a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx +++ b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx @@ -34,6 +34,7 @@ import Flex from "antd/es/flex"; import React from "react"; import dayjs from "dayjs"; import { currentApplication } from "@lowcoder-ee/redux/selectors/applicationSelector"; +import { notificationInstance } from "components/GlobalInstances"; const AppSnapshot = lazy(() => { return import("pages/editor/appSnapshot") @@ -115,10 +116,8 @@ const AppEditor = React.memo(() => { if (currentUser && application) { const lastEditedAt = dayjs(application?.lastEditedAt); const lastEditedDiff = dayjs().diff(lastEditedAt, 'minutes'); - // const shouldBlockEditing = currentUser.id !== application?.createBy && lastEditedDiff < 5; - const shouldBlockEditing = lastEditedDiff < 5; + const shouldBlockEditing = currentUser.id !== application?.editingUserId && lastEditedDiff < 3; setBlockEditing(shouldBlockEditing); - console.log('blockEditing', shouldBlockEditing, {user_id: currentUser.id, editingUserId: application.createBy, lastEditedDiff}); } }, [application, currentUser]); @@ -189,15 +188,21 @@ const AppEditor = React.memo(() => { fetchApplication(); }, [fetchApplication]); - // useEffect(() => { - // if(!blockEditing) return clearInterval(fetchInterval.current); - // if(blockEditing) { - // fetchInterval.current = window.setInterval(() => { - // fetchApplication(); - // }, 60000); - // } - // return () => clearInterval(fetchInterval.current); - // }, [blockEditing, fetchApplication]); + useEffect(() => { + if(!blockEditing && fetchInterval.current) { + notificationInstance.info({ + message: 'Editing Enabled', + description: 'Editing is now enabled. You can proceed with your changes.' + }); + return clearInterval(fetchInterval.current); + } + if(blockEditing) { + fetchInterval.current = window.setInterval(() => { + fetchApplication(); + }, 60000); + } + return () => clearInterval(fetchInterval.current); + }, [blockEditing, fetchApplication]); const fallbackUI = useMemo(() => ( Date: Thu, 19 Sep 2024 22:57:59 +0500 Subject: [PATCH 7/9] added popover to show countdown and check editing status button --- .../lowcoder/src/pages/common/header.tsx | 92 ++++++++++++++++--- .../lowcoder/src/pages/editor/AppEditor.tsx | 18 +--- .../src/pages/editor/appEditorInternal.tsx | 5 +- .../src/util/context/ExternalEditorContext.ts | 4 + .../lowcoder/src/util/editoryHistory.ts | 2 +- 5 files changed, 89 insertions(+), 32 deletions(-) diff --git a/client/packages/lowcoder/src/pages/common/header.tsx b/client/packages/lowcoder/src/pages/common/header.tsx index 06cbf8b2c..019a4cdb6 100644 --- a/client/packages/lowcoder/src/pages/common/header.tsx +++ b/client/packages/lowcoder/src/pages/common/header.tsx @@ -1,6 +1,10 @@ import { default as Dropdown } from "antd/es/dropdown"; import { default as Skeleton } from "antd/es/skeleton"; import { default as Radio, RadioChangeEvent } from "antd/es/radio"; +import { default as Statistic} from "antd/es/statistic"; +import { default as Flex} from "antd/es/flex"; +import { default as Popover } from "antd/es/popover"; +import { default as Typography } from "antd/es/typography"; import LayoutHeader from "components/layout/Header"; import { SHARE_TITLE } from "constants/apiConstants"; import { AppTypeEnum } from "constants/applicationConstants"; @@ -20,12 +24,13 @@ import { Middle, ModuleIcon, PackUpIcon, + RefreshIcon, Right, TacoButton, } from "lowcoder-design"; import { trans } from "i18n"; import dayjs from "dayjs"; -import { useContext, useEffect, useMemo, useState } from "react"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { publishApplication, @@ -58,7 +63,10 @@ import { LockOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; import Avatar from 'antd/es/avatar'; import UserApi from "@lowcoder-ee/api/userApi"; import { validateResponse } from "@lowcoder-ee/api/apiUtils"; +import ProfileImage from "./profileImage"; +const { Countdown } = Statistic; +const { Text } = Typography; const StyledLink = styled.a` display: flex; @@ -186,6 +194,10 @@ const GrayBtn = styled(TacoButton)` color: #ffffff; border: none; } + + &[disabled] { + cursor: not-allowed; + } } `; @@ -282,6 +294,23 @@ const WarningIcon = styled(ExclamationCircleOutlined)` color: #ff4d4f; /* Red color for the icon */ `; +const StyledCountdown = styled(Countdown)` + .ant-statistic-content { + color: #ff4d4f; + margin-top: 2px; + text-align: center; + } +`; + +const StyledRefreshIcon = styled(RefreshIcon)` + width: 16px !important; + height: 16px !important; + margin-right: -3px !important; + > g > g { + stroke: white; + } +`; + // Add the lock icon logic for disabled options const DropdownMenuStyled = styled(DropdownMenu)` .ant-dropdown-menu-item:hover { @@ -314,6 +343,8 @@ function HeaderProfile(props: { user: User }) { ); } +const setCountdown = () => dayjs().add(3, 'minutes').toISOString(); + export type PanelStatus = { left: boolean; bottom: boolean; right: boolean }; export type TogglePanel = (panel?: keyof PanelStatus) => void; @@ -332,7 +363,7 @@ type HeaderProps = { // header in editor page export default function Header(props: HeaderProps) { const editorState = useContext(EditorContext); - const { blockEditing } = useContext(ExternalEditorContext); + const { blockEditing, fetchApplication } = useContext(ExternalEditorContext); const { togglePanel } = props; const { toggleEditorModeStatus } = props; const { left, bottom, right } = props.panelStatus; @@ -347,6 +378,8 @@ export default function Header(props: HeaderProps) { const [editing, setEditing] = useState(false); const [permissionDialogVisible, setPermissionDialogVisible] = useState(false); const [editingUser, setEditingUser] = useState(); + const [enableCheckEditingStatus, setEnableCheckEditingStatus] = useState(false); + const editingCountdown = useRef(setCountdown()); const isModule = appType === AppTypeEnum.Module; @@ -434,8 +467,6 @@ export default function Header(props: HeaderProps) { const headerMiddle = ( <> - <> - {/* Display a hint about who is editing the app */} {blockEditing && ( - + { + return ( + + + Changes will not be saved while another
user is editing this app. +
+ { + setEnableCheckEditingStatus(true) + }} + /> + + { + fetchApplication?.(); + setEnableCheckEditingStatus(false); + editingCountdown.current = setCountdown(); + }} + > + + Check Editing Status + + +
+ ) + }} + trigger="hover" > - + - {/* {`${user.username} is currently editing this app.`} */} - {`${editingUser?.name || 'Someone'} is currently editing this app`} + {`${editingUser?.name || 'Someone'} is editing this app`} -
+ + )} {applicationId && ( @@ -529,7 +594,7 @@ export default function Header(props: HeaderProps) { /> )} {canManageApp(user, application) && ( - setPermissionDialogVisible(true)}> + setPermissionDialogVisible(true)} disabled={blockEditing}> {SHARE_TITLE} )} @@ -596,6 +661,7 @@ export default function Header(props: HeaderProps) { applicationId, permissionDialogVisible, blockEditing, // Include the state in the dependency array + enableCheckEditingStatus, editingUser?.name, ]); diff --git a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx index fee04fda9..c9e03a701 100644 --- a/client/packages/lowcoder/src/pages/editor/AppEditor.tsx +++ b/client/packages/lowcoder/src/pages/editor/AppEditor.tsx @@ -76,7 +76,6 @@ const AppEditor = React.memo(() => { ); const firstRendered = useRef(false); - const fetchInterval = useRef(0); const orgId = useMemo(() => currentUser.currentOrgId, [currentUser.currentOrgId]); const [isDataSourcePluginRegistered, setIsDataSourcePluginRegistered] = useState(false); const [appError, setAppError] = useState(''); @@ -188,22 +187,6 @@ const AppEditor = React.memo(() => { fetchApplication(); }, [fetchApplication]); - useEffect(() => { - if(!blockEditing && fetchInterval.current) { - notificationInstance.info({ - message: 'Editing Enabled', - description: 'Editing is now enabled. You can proceed with your changes.' - }); - return clearInterval(fetchInterval.current); - } - if(blockEditing) { - fetchInterval.current = window.setInterval(() => { - fetchApplication(); - }, 60000); - } - return () => clearInterval(fetchInterval.current); - }, [blockEditing, fetchApplication]); - const fallbackUI = useMemo(() => ( { !fetchOrgGroupsFinished || !isDataSourcePluginRegistered || isCommonSettingsFetching } compInstance={compInstance} + fetchApplication={fetchApplication} /> )} diff --git a/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx b/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx index b49a76dfc..f80f3fd47 100644 --- a/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx +++ b/client/packages/lowcoder/src/pages/editor/appEditorInternal.tsx @@ -79,13 +79,14 @@ interface AppEditorInternalViewProps { appInfo: AppSummaryInfo; loading: boolean; compInstance: RootCompInstanceType; + fetchApplication?: () => void; } export const AppEditorInternalView = React.memo((props: AppEditorInternalViewProps) => { const isUserViewMode = useUserViewMode(); const extraExternalEditorState = useSelector(getExternalEditorState); const dispatch = useDispatch(); - const { readOnly, blockEditing, appInfo, compInstance } = props; + const { readOnly, blockEditing, appInfo, compInstance, fetchApplication } = props; const [externalEditorState, setExternalEditorState] = useState({ changeExternalState: (state: Partial) => { @@ -104,6 +105,7 @@ export const AppEditorInternalView = React.memo((props: AppEditorInternalViewPro applicationId: appInfo.id, hideHeader: window.location.pathname.split("/")[3] === "admin", blockEditing, + fetchApplication: fetchApplication, ...extraExternalEditorState, })); }, [ @@ -112,6 +114,7 @@ export const AppEditorInternalView = React.memo((props: AppEditorInternalViewPro readOnly, appInfo.appType, appInfo.id, blockEditing, + fetchApplication, ]); useEffect(() => { diff --git a/client/packages/lowcoder/src/util/context/ExternalEditorContext.ts b/client/packages/lowcoder/src/util/context/ExternalEditorContext.ts index f1c64f8a6..ab1ad3a5c 100644 --- a/client/packages/lowcoder/src/util/context/ExternalEditorContext.ts +++ b/client/packages/lowcoder/src/util/context/ExternalEditorContext.ts @@ -42,6 +42,10 @@ export interface ExternalEditorContextState { * whether to block editing if someone else is editing the app */ blockEditing?: boolean; + /** + * passing this function to refresh app from header + */ + fetchApplication?: () => void; changeExternalState?: (state: Partial) => void; } diff --git a/client/packages/lowcoder/src/util/editoryHistory.ts b/client/packages/lowcoder/src/util/editoryHistory.ts index 24544f8fc..3f5a8496c 100644 --- a/client/packages/lowcoder/src/util/editoryHistory.ts +++ b/client/packages/lowcoder/src/util/editoryHistory.ts @@ -46,7 +46,7 @@ export function useAppHistory( showCost("addHistory", () => addHistory(actions)); }); return history; - }, [appId, compContainer, reduxDispatch, readOnly]); + }, [appId, compContainer, reduxDispatch, readOnly, blockEditing]); } /** From 10b5c358f4d768e6b8e5c359e722fc5953c994d4 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Thu, 19 Sep 2024 23:19:05 +0200 Subject: [PATCH 8/9] Small Text Changes --- .gitignore | 1 + .vscode/settings.json | 3 ++- client/packages/lowcoder/src/pages/common/header.tsx | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index afe5fd86a..8758dff24 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ application-dev-localhost.yaml .vscode/launch.json server/api-service/lowcoder-server/src/main/resources/application-local-dev.yaml translations/locales/node_modules/ +.vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 9cae45514..495ac31a0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ "titleBar.activeBackground": "#3B431A", "titleBar.activeForeground": "#F9FAF2" }, - "java.debug.settings.onBuildFailureProceed": true + "java.debug.settings.onBuildFailureProceed": true, + "java.configuration.updateBuildConfiguration": "automatic" } \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/common/header.tsx b/client/packages/lowcoder/src/pages/common/header.tsx index 019a4cdb6..8391cf0ce 100644 --- a/client/packages/lowcoder/src/pages/common/header.tsx +++ b/client/packages/lowcoder/src/pages/common/header.tsx @@ -544,14 +544,14 @@ export default function Header(props: HeaderProps) { Changes will not be saved while another
user is editing this app. { setEnableCheckEditingStatus(true) }} /> - Check Editing Status + Check App Status
@@ -576,7 +576,7 @@ export default function Header(props: HeaderProps) { - {`${editingUser?.name || 'Someone'} is editing this app`} + {`${editingUser?.email || 'Someone'} is editing this app`} From 975bcf86b181ba7ccba39371d894111262a4d2da Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Fri, 20 Sep 2024 15:03:31 +0200 Subject: [PATCH 9/9] Text Translations for App Editor concurency Display --- client/packages/lowcoder/src/i18n/locales/de.ts | 8 +++++++- client/packages/lowcoder/src/i18n/locales/en.ts | 8 +++++++- client/packages/lowcoder/src/i18n/locales/es.ts | 6 ++++++ client/packages/lowcoder/src/i18n/locales/it.ts | 6 ++++++ client/packages/lowcoder/src/i18n/locales/pt.ts | 6 ++++++ client/packages/lowcoder/src/i18n/locales/ru.ts | 6 ++++++ client/packages/lowcoder/src/i18n/locales/zh.ts | 8 +++++++- .../lowcoder/src/pages/common/header.tsx | 16 +++++++++------- 8 files changed, 54 insertions(+), 10 deletions(-) diff --git a/client/packages/lowcoder/src/i18n/locales/de.ts b/client/packages/lowcoder/src/i18n/locales/de.ts index e85153aa5..fbf0ed6fe 100644 --- a/client/packages/lowcoder/src/i18n/locales/de.ts +++ b/client/packages/lowcoder/src/i18n/locales/de.ts @@ -2986,7 +2986,13 @@ export const de = { "editorMode_both": "Beide", "editorMode_layout_tooltip": "Passen Sie im rechten Fenster das Erscheinungsbild und Layout der Komponente an. Passen Sie Aussehen, Stil und Animationen an.", "editorMode_logic_tooltip": "Richten Sie im rechten Fenster ein, wie Ihre Komponente funktioniert und interagiert. Verwalten Sie den Inhalt und das interaktive Verhalten.", - }, + "AppEditingBlocked": "App-Bearbeitung blockiert für:", + "AppEditingBlockedHint": "Änderungen werden nicht gespeichert, während ein anderer Benutzer diese App bearbeitet.", + "AppEditingBlockedMessage": "Bitte warten Sie, bevor Sie den Bearbeitungsstatus der App überprüfen.", + "AppEditingBlockedCheckStatus": "App-Status prüfen", + "AppEditingBlockedSomeone": "Jemand", + "AppEditingBlockedMessageSnipped": "bearbeitet diese App", +}, "userAuth": { ...en.userAuth, diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 502468233..f2d94d0d3 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -2922,7 +2922,13 @@ export const en = { "editorMode_logic": "Logic", "editorMode_both": "Both", "editorMode_layout_tooltip": "Customise the component's appearance and layout, in the right window. Adjust how it looks, styles, and animates.", - "editorMode_logic_tooltip": "Set up how your component works and interacts, in the right window. Manage its content and interactive behaviours." + "editorMode_logic_tooltip": "Set up how your component works and interacts, in the right window. Manage its content and interactive behaviours.", + "AppEditingBlocked": "App Editing Blocked for:", + "AppEditingBlockedHint": "Changes will not be saved while another user is editing this app.", + "AppEditingBlockedMessage": "Please wait before check the editing status of the App.", + "AppEditingBlockedCheckStatus": "Check App Status", + "AppEditingBlockedSomeone": "Someone", + "AppEditingBlockedMessageSnipped": "is editing this app", }, "userAuth": { "registerByEmail": "Sign Up", diff --git a/client/packages/lowcoder/src/i18n/locales/es.ts b/client/packages/lowcoder/src/i18n/locales/es.ts index c87c4a7ce..73b07120b 100644 --- a/client/packages/lowcoder/src/i18n/locales/es.ts +++ b/client/packages/lowcoder/src/i18n/locales/es.ts @@ -2986,6 +2986,12 @@ export const es = { "editorMode_both": "Ambos", "editorMode_layout_tooltip": "Personalice la apariencia y el diseño del componente en la ventana derecha. Ajusta su aspecto, estilo y animación.", "editorMode_logic_tooltip": "Configure el funcionamiento y la interacción de su componente, en la ventana adecuada. Gestione su contenido y sus comportamientos interactivos.", + "AppEditingBlocked": "Edición de la App bloqueada para:", + "AppEditingBlockedHint": "Los cambios no se guardarán mientras otro usuario esté editando esta aplicación.", + "AppEditingBlockedMessage": "Por favor, espere antes de verificar el estado de edición de la aplicación.", + "AppEditingBlockedCheckStatus": "Verificar estado de la aplicación", + "AppEditingBlockedSomeone": "Alguien", + "AppEditingBlockedMessageSnipped": "está editando esta aplicación", }, "userAuth": { ...en.userAuth, diff --git a/client/packages/lowcoder/src/i18n/locales/it.ts b/client/packages/lowcoder/src/i18n/locales/it.ts index c0e56de33..8d66e9957 100644 --- a/client/packages/lowcoder/src/i18n/locales/it.ts +++ b/client/packages/lowcoder/src/i18n/locales/it.ts @@ -2986,6 +2986,12 @@ export const it = { "editorMode_both": "Entrambi", "editorMode_layout_tooltip": "Personalizzare l'aspetto e il layout del componente, nella finestra di destra. Regolate l'aspetto, gli stili e le animazioni.", "editorMode_logic_tooltip": "Impostate il funzionamento e l'interazione del vostro componente, nella finestra giusta. Gestite il suo contenuto e i suoi comportamenti interattivi.", + "AppEditingBlocked": "Modifica dell'app bloccata per:", + "AppEditingBlockedHint": "Le modifiche non verranno salvate mentre un altro utente sta modificando questa app.", + "AppEditingBlockedMessage": "Attendere prima di controllare lo stato di modifica dell'app.", + "AppEditingBlockedCheckStatus": "Controlla lo stato dell'app", + "AppEditingBlockedSomeone": "Qualcuno", + "AppEditingBlockedMessageSnipped": "sta modificando questa app", }, "userAuth": { ...en.userAuth, diff --git a/client/packages/lowcoder/src/i18n/locales/pt.ts b/client/packages/lowcoder/src/i18n/locales/pt.ts index 7be71ee23..69a17936b 100644 --- a/client/packages/lowcoder/src/i18n/locales/pt.ts +++ b/client/packages/lowcoder/src/i18n/locales/pt.ts @@ -2986,6 +2986,12 @@ export const pt = { "editorMode_both": "Ambos", "editorMode_layout_tooltip": "Personalize a aparência e o layout do componente, na janela direita. Ajuste sua aparência, estilo e animação.", "editorMode_logic_tooltip": "Configure como seu componente funciona e interage, na janela direita. Gerencie seu conteúdo e comportamentos interativos.", + "AppEditingBlocked": "Edição do aplicativo bloqueada para:", + "AppEditingBlockedHint": "As alterações não serão salvas enquanto outro usuário estiver editando este aplicativo.", + "AppEditingBlockedMessage": "Por favor, aguarde antes de verificar o status de edição do aplicativo.", + "AppEditingBlockedCheckStatus": "Verificar status do aplicativo", + "AppEditingBlockedSomeone": "Alguém", + "AppEditingBlockedMessageSnipped": "está editando este aplicativo", }, "userAuth": { ...en.userAuth, diff --git a/client/packages/lowcoder/src/i18n/locales/ru.ts b/client/packages/lowcoder/src/i18n/locales/ru.ts index fca96eed8..36a817c07 100644 --- a/client/packages/lowcoder/src/i18n/locales/ru.ts +++ b/client/packages/lowcoder/src/i18n/locales/ru.ts @@ -2986,6 +2986,12 @@ export const ru = { "editorMode_both": "Оба", "editorMode_layout_tooltip": "Настройте внешний вид и расположение компонента в правом окне. Настройте его внешний вид, стили и анимацию.", "editorMode_logic_tooltip": "Настройте работу и взаимодействие вашего компонента в нужном окне. Управляйте его содержимым и интерактивным поведением.", + "AppEditingBlocked": "Редактирование приложения заблокировано для:", + "AppEditingBlockedHint": "Изменения не будут сохранены, пока другой пользователь редактирует это приложение.", + "AppEditingBlockedMessage": "Пожалуйста, подождите, прежде чем проверить статус редактирования приложения.", + "AppEditingBlockedCheckStatus": "Проверить статус приложения", + "AppEditingBlockedSomeone": "Кто-то", + "AppEditingBlockedMessageSnipped": "редактирует это приложение", }, "userAuth": { ...en.userAuth, diff --git a/client/packages/lowcoder/src/i18n/locales/zh.ts b/client/packages/lowcoder/src/i18n/locales/zh.ts index 80fcbd7e9..78133ccda 100644 --- a/client/packages/lowcoder/src/i18n/locales/zh.ts +++ b/client/packages/lowcoder/src/i18n/locales/zh.ts @@ -2352,7 +2352,13 @@ export const zh: typeof en = { editorMode_logic: "应用程序逻辑", editorMode_both: "两者", editorMode_layout_tooltip: "在右侧窗口中自定义组件的外观和布局。调整它的外观、样式和动画。", - editorMode_logic_tooltip: "在右侧窗口中设置组件的工作和交互方式。管理其内容和交互行为。" + editorMode_logic_tooltip: "在右侧窗口中设置组件的工作和交互方式。管理其内容和交互行为。", + "AppEditingBlocked": "应用编辑已被阻止:", + "AppEditingBlockedHint": "在其他用户编辑此应用时,无法保存更改。", + "AppEditingBlockedMessage": "请稍候再检查应用的编辑状态。", + "AppEditingBlockedCheckStatus": "检查应用状态", + "AppEditingBlockedSomeone": "某人", + "AppEditingBlockedMessageSnipped": "正在编辑此应用", }, userAuth: { ...en.userAuth, diff --git a/client/packages/lowcoder/src/pages/common/header.tsx b/client/packages/lowcoder/src/pages/common/header.tsx index 8391cf0ce..21844023e 100644 --- a/client/packages/lowcoder/src/pages/common/header.tsx +++ b/client/packages/lowcoder/src/pages/common/header.tsx @@ -388,6 +388,8 @@ export default function Header(props: HeaderProps) { UserApi.getUserDetail(application.editingUserId!) .then(resp => { if (validateResponse(resp)) { + + console.log('editing user', resp.data.data); setEditingUser(resp.data.data); } }); @@ -539,19 +541,19 @@ export default function Header(props: HeaderProps) { style={{ width: 200 }} content={() => { return ( - - - Changes will not be saved while another
user is editing this app. + + + {trans("header.AppEditingBlockedHint")} { setEnableCheckEditingStatus(true) }} /> - Check App Status + {trans("header.AppEditingBlockedCheckStatus")} @@ -576,7 +578,7 @@ export default function Header(props: HeaderProps) { - {`${editingUser?.email || 'Someone'} is editing this app`} + {`${editingUser?.email || trans("header.AppEditingBlockedSomeone")}` + " " + trans("header.AppEditingBlockedMessageSnipped")}