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
added popover to show countdown and check editing status button
  • Loading branch information
raheeliftikhar5 committed Sep 19, 2024
commit 1d2b53da033b3f945de644c0182166d024d4b522
92 changes: 79 additions & 13 deletions client/packages/lowcoder/src/pages/common/header.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -186,6 +194,10 @@ const GrayBtn = styled(TacoButton)`
color: #ffffff;
border: none;
}

&[disabled] {
cursor: not-allowed;
}
}
`;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -347,6 +378,8 @@ export default function Header(props: HeaderProps) {
const [editing, setEditing] = useState(false);
const [permissionDialogVisible, setPermissionDialogVisible] = useState(false);
const [editingUser, setEditingUser] = useState<CurrentUser>();
const [enableCheckEditingStatus, setEnableCheckEditingStatus] = useState<boolean>(false);
const editingCountdown = useRef(setCountdown());

const isModule = appType === AppTypeEnum.Module;

Expand Down Expand Up @@ -434,8 +467,6 @@ export default function Header(props: HeaderProps) {

const headerMiddle = (
<>
<>
</>
<Radio.Group
onChange={onEditorStateValueChange}
value={props.editorModeStatus}
Expand Down Expand Up @@ -503,20 +534,54 @@ export default function Header(props: HeaderProps) {
<>
{/* Display a hint about who is editing the app */}
{blockEditing && (
<Tooltip
title="Changes will not be saved while another user is editing this app."
color="red"
placement="bottom"
<>
<Popover
style={{ width: 200 }}
content={() => {
return (
<Flex vertical gap={10} align="center">
<Text>
Changes will not be saved while another <br/> user is editing this app.
</Text>
<StyledCountdown
title="Editing Availability"
value={editingCountdown.current}
onFinish={() => {
setEnableCheckEditingStatus(true)
}}
/>
<Tooltip
title="You will be able to check the editing status after the countdown."
placement="bottom"
>
<TacoButton
style={{width: '100%'}}
buttonType="primary"
disabled={blockEditing && !enableCheckEditingStatus}
onClick={() => {
fetchApplication?.();
setEnableCheckEditingStatus(false);
editingCountdown.current = setCountdown();
}}
>
<StyledRefreshIcon />
<span>Check Editing Status</span>
</TacoButton>
</Tooltip>
</Flex>
)
}}
trigger="hover"
>
<EditingNoticeWrapper>
<Avatar size="small" src={user.avatarUrl} />
<ProfileImage source={user.avatarUrl} userName={user.username} side={24} />
<EditingHintText>
{/* {`${user.username} is currently editing this app.`} */}
{`${editingUser?.name || 'Someone'} is currently editing this app`}
{`${editingUser?.name || 'Someone'} is editing this app`}
</EditingHintText>
<WarningIcon />
</EditingNoticeWrapper>
</Tooltip>
</Popover>
</>
)}

{applicationId && (
Expand All @@ -529,7 +594,7 @@ export default function Header(props: HeaderProps) {
/>
)}
{canManageApp(user, application) && (
<GrayBtn onClick={() => setPermissionDialogVisible(true)}>
<GrayBtn onClick={() => setPermissionDialogVisible(true)} disabled={blockEditing}>
{SHARE_TITLE}
</GrayBtn>
)}
Expand Down Expand Up @@ -596,6 +661,7 @@ export default function Header(props: HeaderProps) {
applicationId,
permissionDialogVisible,
blockEditing, // Include the state in the dependency array
enableCheckEditingStatus,
editingUser?.name,
]);

Expand Down
18 changes: 1 addition & 17 deletions client/packages/lowcoder/src/pages/editor/AppEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ const AppEditor = React.memo(() => {
);

const firstRendered = useRef(false);
const fetchInterval = useRef<number>(0);
const orgId = useMemo(() => currentUser.currentOrgId, [currentUser.currentOrgId]);
const [isDataSourcePluginRegistered, setIsDataSourcePluginRegistered] = useState(false);
const [appError, setAppError] = useState('');
Expand Down Expand Up @@ -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(() => (
<Flex align="center" justify="center" vertical style={{
height: '300px',
Expand Down Expand Up @@ -249,6 +232,7 @@ const AppEditor = React.memo(() => {
!fetchOrgGroupsFinished || !isDataSourcePluginRegistered || isCommonSettingsFetching
}
compInstance={compInstance}
fetchApplication={fetchApplication}
/>
</Suspense>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExternalEditorContextState>({
changeExternalState: (state: Partial<ExternalEditorContextState>) => {
Expand All @@ -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,
}));
}, [
Expand All @@ -112,6 +114,7 @@ export const AppEditorInternalView = React.memo((props: AppEditorInternalViewPro
readOnly,
appInfo.appType, appInfo.id,
blockEditing,
fetchApplication,
]);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExternalEditorContextState>) => void;
}
Expand Down
2 changes: 1 addition & 1 deletion client/packages/lowcoder/src/util/editoryHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function useAppHistory(
showCost("addHistory", () => addHistory(actions));
});
return history;
}, [appId, compContainer, reduxDispatch, readOnly]);
}, [appId, compContainer, reduxDispatch, readOnly, blockEditing]);
}

/**
Expand Down
Loading