From e4f5afc0c043de61866eece678ce14fa01ae1d23 Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Thu, 23 Nov 2023 14:03:22 +0200 Subject: [PATCH] Merge pull request #15829 from overleaf/ii-ide-page-prototype-permissions [web] React ide page permissions GitOrigin-RevId: 727d33e0654d4bfefe5e710e553895f793cacb82 --- .../connection/join-project-payload.ts | 2 +- .../ide-react/context/metadata-context.tsx | 6 +- .../ide-react/context/permissions-context.tsx | 117 ++++++++++++++++++ .../ide-react/context/react-context-root.tsx | 41 +++--- .../hooks/use-review-panel-state.ts | 8 +- .../ide-react/create-ide-event-emitter.ts | 2 +- .../ide-react/types/permissions-level.ts | 1 - .../features/ide-react/types/permissions.ts | 8 ++ .../review-panel/entries/comment-entry.tsx | 8 +- .../entries/resolved-comment-entry.tsx | 4 +- .../toolbar/resolved-comments-scroller.tsx | 4 +- .../types/base-change-entry-props.ts | 4 +- .../review-panel/types/review-panel-state.ts | 4 +- .../web/types/review-panel/review-panel.ts | 7 -- 14 files changed, 168 insertions(+), 48 deletions(-) create mode 100644 services/web/frontend/js/features/ide-react/context/permissions-context.tsx delete mode 100644 services/web/frontend/js/features/ide-react/types/permissions-level.ts create mode 100644 services/web/frontend/js/features/ide-react/types/permissions.ts diff --git a/services/web/frontend/js/features/ide-react/connection/join-project-payload.ts b/services/web/frontend/js/features/ide-react/connection/join-project-payload.ts index a06f80f20b..5fa4aab408 100644 --- a/services/web/frontend/js/features/ide-react/connection/join-project-payload.ts +++ b/services/web/frontend/js/features/ide-react/connection/join-project-payload.ts @@ -1,5 +1,5 @@ import { Project } from '../../../../../types/project' -import { PermissionsLevel } from '../types/permissions-level' +import { PermissionsLevel } from '@/features/ide-react/types/permissions' export type JoinProjectPayloadProject = Pick< Project, diff --git a/services/web/frontend/js/features/ide-react/context/metadata-context.tsx b/services/web/frontend/js/features/ide-react/context/metadata-context.tsx index a3d3bca3b8..66d07bcc5c 100644 --- a/services/web/frontend/js/features/ide-react/context/metadata-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/metadata-context.tsx @@ -21,6 +21,7 @@ import useEventListener from '@/shared/hooks/use-event-listener' import { FileTreeFindResult } from '@/features/ide-react/types/file-tree' import { Project } from '../../../../../types/project' import { useModalsContext } from '@/features/ide-react/context/modals-context' +import { usePermissionsContext } from '@/features/ide-react/context/permissions-context' import { useTranslation } from 'react-i18next' type DocumentMetadata = { @@ -53,6 +54,7 @@ export const MetadataProvider: FC = ({ children }) => { const { socket } = useConnectionContext() const { onlineUsersCount } = useOnlineUsersContext() const { permissionsLevel } = useEditorContext() + const { permissions } = usePermissionsContext() const { currentDocument } = useEditorManagerContext() const { showGenericMessageModal } = useModalsContext() @@ -190,7 +192,7 @@ export const MetadataProvider: FC = ({ children }) => { ) } window.setTimeout(() => { - if (permissionsLevel !== 'readOnly') { + if (permissions.write) { loadProjectMetaFromServer() } }, 200) @@ -204,7 +206,7 @@ export const MetadataProvider: FC = ({ children }) => { }, [ eventEmitter, loadProjectMetaFromServer, - permissionsLevel, + permissions, showGenericMessageModal, t, ]) diff --git a/services/web/frontend/js/features/ide-react/context/permissions-context.tsx b/services/web/frontend/js/features/ide-react/context/permissions-context.tsx new file mode 100644 index 0000000000..5355cca7b7 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/context/permissions-context.tsx @@ -0,0 +1,117 @@ +import { createContext, useContext, useState, useEffect, useMemo } from 'react' +import { useConnectionContext } from '@/features/ide-react/context/connection-context' +import { useEditorContext } from '@/shared/context/editor-context' +import getMeta from '@/utils/meta' +import { Permissions } from '@/features/ide-react/types/permissions' + +type PermissionsContextValue = { + permissions: Permissions +} + +const PermissionsContext = createContext( + undefined +) + +const readOnlyPermissions: Readonly = { + read: true, + write: false, + admin: false, + comment: true, +} +const readAndWritePermissions: Readonly = { + read: true, + write: true, + admin: false, + comment: true, +} +const ownerPermissions: Readonly = { + read: true, + write: true, + admin: true, + comment: true, +} +const permissionsMap = { + readOnly: readOnlyPermissions, + readAndWrite: readAndWritePermissions, + owner: ownerPermissions, + anonymous: { + readOnly: { ...readOnlyPermissions, comment: false }, + readAndWrite: { ...readAndWritePermissions, comment: false }, + owner: { ...ownerPermissions, comment: false }, + }, +} as const + +export const PermissionsProvider: React.FC = ({ children }) => { + const [permissions, setPermissions] = useState({ + read: false, + write: false, + admin: false, + comment: false, + }) + const { connectionState } = useConnectionContext() + const { permissionsLevel } = useEditorContext() + const anonymous = getMeta('ol-anonymous') as boolean | undefined + + useEffect(() => { + if (permissionsLevel === 'readOnly') { + if (anonymous) { + setPermissions(permissionsMap.anonymous.readOnly) + } else { + setPermissions(permissionsMap.readOnly) + } + } + if (permissionsLevel === 'readAndWrite') { + if (permissions.admin) { + if (anonymous) { + setPermissions(permissionsMap.anonymous.owner) + } else { + setPermissions(permissionsMap.owner) + } + } else { + if (anonymous) { + setPermissions(permissionsMap.anonymous.readAndWrite) + } else { + setPermissions(permissionsMap.readAndWrite) + } + } + } + if (permissionsLevel === 'owner') { + if (anonymous) { + setPermissions(permissionsMap.anonymous.owner) + } else { + setPermissions(permissionsMap.owner) + } + } + }, [anonymous, permissions, permissionsLevel]) + + useEffect(() => { + if (connectionState.forceDisconnected) { + setPermissions(prevState => ({ ...prevState, write: false })) + } + }, [connectionState.forceDisconnected]) + + const value = useMemo( + () => ({ + permissions, + }), + [permissions] + ) + + return ( + + {children} + + ) +} + +export function usePermissionsContext(): PermissionsContextValue { + const context = useContext(PermissionsContext) + + if (!context) { + throw new Error( + 'usePermissionsContext is only available inside PermissionsProvider' + ) + } + + return context +} diff --git a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx index b46ae52470..29273f2465 100644 --- a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx +++ b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx @@ -19,6 +19,7 @@ import { SplitTestProvider } from '@/shared/context/split-test-context' import { ModalsContextProvider } from '@/features/ide-react/context/modals-context' import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path' import { UserSettingsProvider } from '@/shared/context/user-settings-context' +import { PermissionsProvider } from '@/features/ide-react/context/permissions-context' export const ReactContextRoot: FC = ({ children }) => { return ( @@ -33,25 +34,27 @@ export const ReactContextRoot: FC = ({ children }) => { - - - - - - - - - - {children} - - - - - - - - - + + + + + + + + + + + {children} + + + + + + + + + + diff --git a/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts b/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts index 151ec924b6..0c8f6af51c 100644 --- a/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts +++ b/services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts @@ -11,13 +11,13 @@ import { useLayoutContext } from '@/shared/context/layout-context' import { useUserContext } from '@/shared/context/user-context' import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context' import { useConnectionContext } from '@/features/ide-react/context/connection-context' +import { usePermissionsContext } from '@/features/ide-react/context/permissions-context' import { debugConsole } from '@/utils/debugging' import { useEditorContext } from '@/shared/context/editor-context' import { getJSON, postJSON } from '@/infrastructure/fetch-json' import ColorManager from '@/ide/colors/ColorManager' // @ts-ignore import RangesTracker from '@overleaf/ranges-tracker' -import { ReviewPanelStateReactIde } from '../types/review-panel-state' import * as ReviewPanel from '../types/review-panel-state' import { ReviewPanelCommentThreadMessage, @@ -28,6 +28,7 @@ import { } from '../../../../../../../types/review-panel/review-panel' import { UserId } from '../../../../../../../types/user' import { PublicAccessLevel } from '../../../../../../../types/public-access-level' +import { ReviewPanelStateReactIde } from '../types/review-panel-state' import { DeepReadonly, MergeAndOverride, @@ -114,6 +115,8 @@ function useReviewPanelState(): ReviewPanelStateReactIde { features: { trackChangesVisible, trackChanges }, } = project const { isRestrictedTokenMember } = useEditorContext() + // TODO permissions to be removed from the review panel context. It currently acts just as a proxy. + const { permissions } = usePermissionsContext() // TODO `currentDocument` and `currentDocumentId` should be get from `useEditorManagerContext()` but that makes tests fail const [currentDocument] = useScopeValue('editor.sharejs_doc') @@ -134,9 +137,6 @@ function useReviewPanelState(): ReviewPanelStateReactIde { ReviewPanel.Value<'commentThreads'> >({}) const [entries, setEntries] = useState>({}) - - const [permissions] = - useScopeValue>('permissions') const [users, setUsers] = useScopeValue>( 'users', true diff --git a/services/web/frontend/js/features/ide-react/create-ide-event-emitter.ts b/services/web/frontend/js/features/ide-react/create-ide-event-emitter.ts index 54d766b7f6..4660a96bdf 100644 --- a/services/web/frontend/js/features/ide-react/create-ide-event-emitter.ts +++ b/services/web/frontend/js/features/ide-react/create-ide-event-emitter.ts @@ -1,6 +1,6 @@ import { Emitter } from 'strict-event-emitter' import { Project } from '../../../../types/project' -import { PermissionsLevel } from '@/features/ide-react/types/permissions-level' +import { PermissionsLevel } from '@/features/ide-react/types/permissions' import { ShareJsDoc } from '@/features/ide-react/editor/share-js-doc' import { GotoLineOptions } from '@/features/ide-react/types/goto-line-options' import { CursorPosition } from '@/features/ide-react/types/cursor-position' diff --git a/services/web/frontend/js/features/ide-react/types/permissions-level.ts b/services/web/frontend/js/features/ide-react/types/permissions-level.ts deleted file mode 100644 index 43a6612653..0000000000 --- a/services/web/frontend/js/features/ide-react/types/permissions-level.ts +++ /dev/null @@ -1 +0,0 @@ -export type PermissionsLevel = 'owner' | 'readAndWrite' | 'readOnly' diff --git a/services/web/frontend/js/features/ide-react/types/permissions.ts b/services/web/frontend/js/features/ide-react/types/permissions.ts new file mode 100644 index 0000000000..531f4d3e79 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/types/permissions.ts @@ -0,0 +1,8 @@ +export type Permissions = { + read: boolean + write: boolean + admin: boolean + comment: boolean +} + +export type PermissionsLevel = 'owner' | 'readAndWrite' | 'readOnly' diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx index 9990ccfa7d..0b8fb5cd39 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx @@ -8,10 +8,8 @@ import AutoExpandingTextArea from '../../../../../shared/components/auto-expandi import Icon from '../../../../../shared/components/icon' import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context' import classnames from 'classnames' -import { - ReviewPanelPermissions, - ThreadId, -} from '../../../../../../../types/review-panel/review-panel' +import { ThreadId } from '../../../../../../../types/review-panel/review-panel' +import { Permissions } from '@/features/ide-react/types/permissions' import { DocId } from '../../../../../../../types/project-settings' import { ReviewPanelCommentThread } from '../../../../../../../types/review-panel/comment-thread' import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel/entry' @@ -24,7 +22,7 @@ type CommentEntryProps = { entryId: ThreadId thread: ReviewPanelCommentThread | undefined threadId: ReviewPanelCommentEntry['thread_id'] - permissions: ReviewPanelPermissions + permissions: Permissions } & Pick function CommentEntry({ diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/entries/resolved-comment-entry.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/entries/resolved-comment-entry.tsx index 9da9830190..09970de1ad 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/entries/resolved-comment-entry.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/entries/resolved-comment-entry.tsx @@ -4,7 +4,7 @@ import Linkify from 'react-linkify' import { formatTime } from '../../../../utils/format-date' import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context' import { FilteredResolvedComments } from '../toolbar/resolved-comments-dropdown' -import { ReviewPanelPermissions } from '../../../../../../../types/review-panel/review-panel' +import { Permissions } from '@/features/ide-react/types/permissions' function LinkDecorator( decoratedHref: string, @@ -20,7 +20,7 @@ function LinkDecorator( type ResolvedCommentEntryProps = { thread: FilteredResolvedComments - permissions: ReviewPanelPermissions + permissions: Permissions contentLimit?: number } diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-scroller.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-scroller.tsx index f62f2a36dd..c306f4c79c 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-scroller.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-scroller.tsx @@ -2,11 +2,11 @@ import { useTranslation } from 'react-i18next' import { useMemo } from 'react' import ResolvedCommentEntry from '../entries/resolved-comment-entry' import { FilteredResolvedComments } from './resolved-comments-dropdown' -import { ReviewPanelPermissions } from '../../../../../../../types/review-panel/review-panel' +import { Permissions } from '@/features/ide-react/types/permissions' type ResolvedCommentsScrollerProps = { resolvedComments: FilteredResolvedComments[] - permissions: ReviewPanelPermissions + permissions: Permissions } function ResolvedCommentsScroller({ diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/types/base-change-entry-props.ts b/services/web/frontend/js/features/source-editor/components/review-panel/types/base-change-entry-props.ts index 0fdfd4bc43..7437482457 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/types/base-change-entry-props.ts +++ b/services/web/frontend/js/features/source-editor/components/review-panel/types/base-change-entry-props.ts @@ -1,16 +1,16 @@ import { ReviewPanelChangeEntry } from '../../../../../../../types/review-panel/entry' import { DocId } from '../../../../../../../types/project-settings' import { - ReviewPanelPermissions, ReviewPanelUser, ThreadId, } from '../../../../../../../types/review-panel/review-panel' +import { Permissions } from '@/features/ide-react/types/permissions' export interface BaseChangeEntryProps extends Pick { docId: DocId entryId: ThreadId - permissions: ReviewPanelPermissions + permissions: Permissions user: ReviewPanelUser | undefined timestamp: ReviewPanelChangeEntry['metadata']['ts'] contentLimit?: number diff --git a/services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts b/services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts index dcc216d507..362c8554c7 100644 --- a/services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts +++ b/services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts @@ -2,11 +2,11 @@ import { CommentId, ReviewPanelCommentThreads, ReviewPanelEntries, - ReviewPanelPermissions, ReviewPanelUsers, SubView, ThreadId, } from '../../../../../../../types/review-panel/review-panel' +import { Permissions } from '@/features/ide-react/types/permissions' import { DocId } from '../../../../../../../types/project-settings' import { dispatchReviewPanelLayout } from '../../../extensions/changes/change-manager' import { UserId } from '../../../../../../../types/user' @@ -21,7 +21,7 @@ export interface ReviewPanelState { isAddingComment: boolean loadingThreads: boolean nVisibleSelectedChanges: number - permissions: ReviewPanelPermissions + permissions: Permissions users: ReviewPanelUsers resolvedComments: ReviewPanelEntries shouldCollapse: boolean diff --git a/services/web/types/review-panel/review-panel.ts b/services/web/types/review-panel/review-panel.ts index d76e099a85..69a5544447 100644 --- a/services/web/types/review-panel/review-panel.ts +++ b/services/web/types/review-panel/review-panel.ts @@ -10,13 +10,6 @@ import { ReviewPanelCommentThread } from './comment-thread' export type SubView = 'cur_file' | 'overview' -export interface ReviewPanelPermissions { - read: boolean - write: boolean - admin: boolean - comment: boolean -} - export type ThreadId = Brand // Entries may contain `add-comment` and `bulk-actions` props along with DocIds // Ideally the `add-comment` and `bulk-actions` objects should not be within the entries object