Skip to content

Concurrency user display #1175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Sep 20, 2024
Prev Previous commit
Next Next commit
Block editing if someone else is editing the app
  • Loading branch information
raheeliftikhar5 committed Sep 19, 2024
commit 8470a4226957c292cc7eb51d67d52830a27127b1
10 changes: 9 additions & 1 deletion client/packages/lowcoder/src/api/applicationApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
PublishApplicationPayload,
RecycleApplicationPayload,
RestoreApplicationPayload,
SetAppEditingStatePayload,
UpdateAppPermissionPayload,
} from "redux/reduxActions/applicationActions";
import { ApiResponse, GenericApiResponse } from "./apiResponses";
Expand Down Expand Up @@ -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<HomeDataResponse> {
return Api.get(ApplicationApi.fetchHomeDataURL, request);
Expand Down Expand Up @@ -232,6 +233,13 @@ class ApplicationApi extends Api {
static getMarketplaceApp(appId: string) {
return Api.get(ApplicationApi.getMarketplaceAppURL(appId));
}

static setAppEditingState(request: SetAppEditingStatePayload): AxiosPromise<ApplicationResp> {
const { applicationId, editingFinished } = request;
return Api.put(ApplicationApi.setAppEditingStateURL(applicationId), {
editingFinished,
});
}
}

export default ApplicationApi;
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export interface ApplicationMeta {
folder: false;
isLocalMarketplace?: boolean;
applicationStatus: "NORMAL" | "RECYCLED" | "DELETED";
editingUserId: string | null;
}

export interface FolderMeta {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
48 changes: 33 additions & 15 deletions client/packages/lowcoder/src/pages/common/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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<CurrentUser>();

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 = [
{
Expand Down Expand Up @@ -491,7 +507,7 @@ export default function Header(props: HeaderProps) {
) : (
<>
{/* Display a hint about who is editing the app */}
{concurrentAppEditingState && (
{blockEditing && (
<Tooltip
title="Changes will not be saved while another user is editing this app."
color="red"
Expand All @@ -500,7 +516,8 @@ export default function Header(props: HeaderProps) {
<EditingNoticeWrapper>
<Avatar size="small" src={user.avatarUrl} />
<EditingHintText>
{`${user.username} is currently editing this app.`}
{/* {`${user.username} is currently editing this app.`} */}
{`${editingUser?.name || 'Someone'} is currently editing this app`}
</EditingHintText>
<WarningIcon />
</EditingNoticeWrapper>
Expand Down Expand Up @@ -534,7 +551,7 @@ export default function Header(props: HeaderProps) {
<DropdownMenuStyled
style={{ minWidth: "110px", borderRadius: "4px" }}
onClick={(e) => {
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") {
Expand All @@ -546,31 +563,31 @@ export default function Header(props: HeaderProps) {
key: "deploy",
label: (
<div style={{ display: 'flex', alignItems: 'center' }}>
{concurrentAppEditingState && <LockOutlined style={{ marginRight: '8px' }} />}
<CommonTextLabel style= {{color: concurrentAppEditingState ? "#ccc" : "#222"}}>
{blockEditing && <LockOutlined style={{ marginRight: '8px' }} />}
<CommonTextLabel style= {{color: blockEditing ? "#ccc" : "#222"}}>
{trans("header.deploy")}
</CommonTextLabel>
</div>
),
disabled: concurrentAppEditingState,
disabled: blockEditing,
},
{
key: "snapshot",
label: (
<div style={{ display: 'flex', alignItems: 'center' }}>
{concurrentAppEditingState && <LockOutlined style={{ marginRight: '8px' }} />}
<CommonTextLabel style= {{color: concurrentAppEditingState ? "#ccc" : "#222"}}>
{blockEditing && <LockOutlined style={{ marginRight: '8px' }} />}
<CommonTextLabel style= {{color: blockEditing ? "#ccc" : "#222"}}>
{trans("header.snapshot")}
</CommonTextLabel>
</div>
),
disabled: concurrentAppEditingState,
disabled: blockEditing,
},
]}
/>
)}
>
<PackUpBtn buttonType="primary" disabled={concurrentAppEditingState}>
<PackUpBtn buttonType="primary" disabled={blockEditing}>
<PackUpIcon />
</PackUpBtn>
</Dropdown>
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
});
13 changes: 13 additions & 0 deletions client/packages/lowcoder/src/redux/sagas/applicationSagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
PublishApplicationPayload,
RecycleApplicationPayload,
RestoreApplicationPayload,
SetAppEditingStatePayload,
UpdateApplicationPayload,
UpdateAppMetaPayload,
UpdateAppPermissionPayload,
Expand Down Expand Up @@ -391,6 +392,17 @@ function* fetchAllMarketplaceAppsSaga() {
}
}

function* setAppEditingStateSaga(action: ReduxAction<SetAppEditingStatePayload>) {
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),
Expand All @@ -416,5 +428,6 @@ export default function* applicationSagas() {
ReduxActionTypes.FETCH_ALL_MARKETPLACE_APPS,
fetchAllMarketplaceAppsSaga,
),
takeLatest(ReduxActionTypes.SET_APP_EDITING_STATE, setAppEditingStateSaga),
]);
}