From 7db5d761ea3e07045c3df87c5135550b8f0b5640 Mon Sep 17 00:00:00 2001 From: ilkin-overleaf <100852799+ilkin-overleaf@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:56:54 +0200 Subject: [PATCH] Merge pull request #15557 from overleaf/ii-ide-page-prototype-review-panel-track-changes Review panel track changes for React IDE page GitOrigin-RevId: d061596581ff10bd897b286dcd5c280ce79a6384 --- .../context/editor-manager-context.tsx | 30 -- .../ide-react/context/ide-react-context.tsx | 2 +- .../hooks/use-review-panel-state.ts | 455 ++++++++++++++++-- .../review-panel/review-panel-context.tsx | 22 +- .../editor-manager-context-adapter.ts | 30 ++ .../review-panel-context-adapter.ts | 9 - .../review-panel/toolbar/toggle-menu.tsx | 4 +- .../review-panel/types/review-panel-state.ts | 13 +- .../js/shared/context/project-context.jsx | 3 + .../web/frontend/stories/decorators/scope.tsx | 4 +- .../codemirror-editor-autocomplete.spec.tsx | 3 +- .../group-subscription-memberships.test.tsx | 5 +- .../managed-group-subscriptions.test.tsx | 5 +- services/web/types/public-access-level.ts | 5 + .../web/types/review-panel/comment-thread.ts | 2 +- services/web/types/review-panel/entry.ts | 3 +- .../web/types/review-panel/review-panel.ts | 3 +- services/web/types/user.ts | 6 +- 18 files changed, 495 insertions(+), 109 deletions(-) create mode 100644 services/web/frontend/js/features/ide-react/scope-adapters/editor-manager-context-adapter.ts create mode 100644 services/web/types/public-access-level.ts diff --git a/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx b/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx index dd61bcd445..4fef0d0aa4 100644 --- a/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx @@ -1,4 +1,3 @@ -import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store' import { createContext, FC, @@ -79,35 +78,6 @@ export type EditorScopeValue = { error_state: boolean } -export function populateEditorScope( - store: ReactScopeValueStore, - projectId: string -) { - // This value is not used in the React code. It's just here to prevent errors - // from EditorProvider - store.set('state.loading', false) - - store.set('project.name', null) - - store.set('editor', { - showSymbolPalette: false, - toggleSymbolPalette: () => {}, - sharejs_doc: null, - open_doc_id: null, - open_doc_name: null, - opening: true, - trackChanges: false, - wantTrackChanges: false, - // No Ace here - newSourceEditor: true, - error_state: false, - }) - store.persisted('editor.showVisual', false, `editor.mode.${projectId}`, { - toPersisted: showVisual => (showVisual ? 'rich-text' : 'source'), - fromPersisted: mode => mode === 'rich-text', - }) -} - const EditorManagerContext = createContext(undefined) export const EditorManagerProvider: FC = ({ children }) => { diff --git a/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx b/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx index 096f7506db..3f0b30b6aa 100644 --- a/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/ide-react-context.tsx @@ -18,7 +18,7 @@ import { import { JoinProjectPayload } from '@/features/ide-react/connection/join-project-payload' import { useConnectionContext } from '@/features/ide-react/context/connection-context' import { getMockIde } from '@/shared/context/mock/mock-ide' -import { populateEditorScope } from '@/features/ide-react/context/editor-manager-context' +import { populateEditorScope } from '@/features/ide-react/scope-adapters/editor-manager-context-adapter' import { postJSON } from '@/infrastructure/fetch-json' import { EventLog } from '@/features/ide-react/editor/event-log' import { populateSettingsScope } from '@/features/ide-react/scope-adapters/settings-adapter' 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 67af94ca1a..1022036dd9 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 @@ -1,21 +1,76 @@ -import { useState, useMemo, useCallback } from 'react' +import { useState, useEffect, useMemo, useCallback, useRef } from 'react' import useScopeValue from '../../../../../shared/hooks/use-scope-value' +import useSocketListener from '@/features/ide-react/hooks/use-socket-listener' import { sendMB } from '../../../../../infrastructure/event-tracking' import { dispatchReviewPanelLayout as handleLayoutChange } from '@/features/source-editor/extensions/changes/change-manager' import { useProjectContext } from '@/shared/context/project-context' 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 { debugConsole } from '@/utils/debugging' +import { postJSON } from '@/infrastructure/fetch-json' import { ReviewPanelStateReactIde } from '../types/review-panel-state' +import ColorManager from '@/ide/colors/ColorManager' import * as ReviewPanel from '../types/review-panel-state' import { SubView, ThreadId, } from '../../../../../../../types/review-panel/review-panel' +import { UserId } from '../../../../../../../types/user' +import { PublicAccessLevel } from '../../../../../../../types/public-access-level' +import { DeepReadonly } from '../../../../../../../types/utils' + +function formatUser(user: any): any { + let isSelf, name + const id = + (user != null ? user._id : undefined) || + (user != null ? user.id : undefined) + + if (id == null) { + return { + email: null, + name: 'Anonymous', + isSelf: false, + hue: ColorManager.ANONYMOUS_HUE, + avatar_text: 'A', + } + } + if (id === window.user_id) { + name = 'You' + isSelf = true + } else { + name = [user.first_name, user.last_name] + .filter(n => n != null && n !== '') + .join(' ') + if (name === '') { + name = + (user.email != null ? user.email.split('@')[0] : undefined) || 'Unknown' + } + isSelf = false + } + return { + id, + email: user.email, + name, + isSelf, + hue: ColorManager.getHueForUserId(id), + avatar_text: [user.first_name, user.last_name] + .filter(n => n != null) + .map(n => n[0]) + .join(''), + } +} function useReviewPanelState(): ReviewPanelStateReactIde { const { reviewPanelOpen, setReviewPanelOpen } = useLayoutContext() + const { projectId } = useIdeReactContext() + const project = useProjectContext() + const user = useUserContext() + const { socket } = useConnectionContext() const { - features: { trackChangesVisible }, - } = useProjectContext() + features: { trackChangesVisible, trackChanges }, + } = project const [subView, setSubView] = useScopeValue>( 'reviewPanel.subView' @@ -47,7 +102,7 @@ function useReviewPanelState(): ReviewPanelStateReactIde { ReviewPanel.Value<'resolvedComments'> >('reviewPanel.resolvedComments', true) - const [wantTrackChanges] = useScopeValue< + const [wantTrackChanges, setWantTrackChanges] = useScopeValue< ReviewPanel.Value<'wantTrackChanges'> >('editor.wantTrackChanges') const [openDocId] = @@ -58,28 +113,337 @@ function useReviewPanelState(): ReviewPanelStateReactIde { 'reviewPanel.rendererData.lineHeight' ) - const [toggleTrackChangesForEveryone] = useScopeValue< - ReviewPanel.UpdaterFn<'toggleTrackChangesForEveryone'> - >('toggleTrackChangesForEveryone') - const [toggleTrackChangesForUser] = useScopeValue< - ReviewPanel.UpdaterFn<'toggleTrackChangesForUser'> - >('toggleTrackChangesForUser') - const [toggleTrackChangesForGuests] = useScopeValue< - ReviewPanel.UpdaterFn<'toggleTrackChangesForGuests'> - >('toggleTrackChangesForGuests') - - const [trackChangesState] = useScopeValue< + const [formattedProjectMembers, setFormattedProjectMembers] = useState< + ReviewPanel.Value<'formattedProjectMembers'> + >({}) + const [trackChangesState, setTrackChangesState] = useState< ReviewPanel.Value<'trackChangesState'> - >('reviewPanel.trackChangesState') - const [trackChangesOnForEveryone] = useScopeValue< - ReviewPanel.Value<'trackChangesOnForEveryone'> - >('reviewPanel.trackChangesOnForEveryone') - const [trackChangesOnForGuests] = useScopeValue< - ReviewPanel.Value<'trackChangesOnForGuests'> - >('reviewPanel.trackChangesOnForGuests') - const [trackChangesForGuestsAvailable] = useScopeValue< - ReviewPanel.Value<'trackChangesForGuestsAvailable'> - >('reviewPanel.trackChangesForGuestsAvailable') + >({}) + const [trackChangesOnForEveryone, setTrackChangesOnForEveryone] = + useState>(false) + const [trackChangesOnForGuests, setTrackChangesOnForGuests] = + useState>(false) + const [trackChangesForGuestsAvailable, setTrackChangesForGuestsAvailable] = + useState>(false) + + const currentUserType = useCallback((): 'member' | 'guest' | 'anonymous' => { + if (!user) { + return 'anonymous' + } + if (project.owner === user.id) { + return 'member' + } + for (const member of project.members as any[]) { + if (member._id === user.id) { + return 'member' + } + } + return 'guest' + }, [project.members, project.owner, user]) + + const applyClientTrackChangesStateToServer = useCallback( + ( + trackChangesOnForEveryone: boolean, + trackChangesOnForGuests: boolean, + trackChangesState: ReviewPanel.Value<'trackChangesState'> + ) => { + const data: { + on?: boolean + on_for?: Record + on_for_guests?: boolean + } = {} + if (trackChangesOnForEveryone) { + data.on = true + } else { + data.on_for = {} + const entries = Object.entries(trackChangesState) as Array< + [ + UserId, + NonNullable< + typeof trackChangesState[keyof typeof trackChangesState] + > + ] + > + for (const [userId, { value }] of entries) { + data.on_for[userId] = value + } + if (trackChangesOnForGuests) { + data.on_for_guests = true + } + } + postJSON(`/project/${projectId}/track_changes`, { + body: data, + }).catch(debugConsole.error) + }, + [projectId] + ) + + const setGuestsTCState = useCallback( + (newValue: boolean) => { + setTrackChangesOnForGuests(newValue) + if (currentUserType() === 'guest' || currentUserType() === 'anonymous') { + setWantTrackChanges(newValue) + } + }, + [currentUserType, setWantTrackChanges] + ) + + const setUserTCState = useCallback( + ( + trackChangesState: DeepReadonly>, + userId: UserId, + newValue: boolean, + isLocal = false + ) => { + const newTrackChangesState: ReviewPanel.Value<'trackChangesState'> = { + ...trackChangesState, + } + const state = + newTrackChangesState[userId] ?? + ({} as NonNullable) + newTrackChangesState[userId] = state + + if (state.syncState == null || state.syncState === 'synced') { + state.value = newValue + state.syncState = 'synced' + } else if (state.syncState === 'pending' && state.value === newValue) { + state.syncState = 'synced' + } else if (isLocal) { + state.value = newValue + state.syncState = 'pending' + } + + setTrackChangesState(newTrackChangesState) + + if (userId === user.id) { + setWantTrackChanges(newValue) + } + + return newTrackChangesState + }, + [setWantTrackChanges, user.id] + ) + + const setEveryoneTCState = useCallback( + (newValue: boolean, isLocal = false) => { + setTrackChangesOnForEveryone(newValue) + let newTrackChangesState: ReviewPanel.Value<'trackChangesState'> = { + ...trackChangesState, + } + for (const member of project.members as any[]) { + newTrackChangesState = setUserTCState( + newTrackChangesState, + member._id, + newValue, + isLocal + ) + } + setGuestsTCState(newValue) + + newTrackChangesState = setUserTCState( + newTrackChangesState, + project.owner._id, + newValue, + isLocal + ) + + return { trackChangesState: newTrackChangesState } + }, + [ + project.members, + project.owner._id, + setGuestsTCState, + setUserTCState, + trackChangesState, + ] + ) + + const toggleTrackChangesForEveryone = useCallback< + ReviewPanel.UpdaterFn<'toggleTrackChangesForEveryone'> + >( + (onForEveryone: boolean) => { + const { trackChangesState } = setEveryoneTCState(onForEveryone, true) + setGuestsTCState(onForEveryone) + applyClientTrackChangesStateToServer( + onForEveryone, + onForEveryone, + trackChangesState + ) + }, + [applyClientTrackChangesStateToServer, setEveryoneTCState, setGuestsTCState] + ) + + const toggleTrackChangesForGuests = useCallback< + ReviewPanel.UpdaterFn<'toggleTrackChangesForGuests'> + >( + (onForGuests: boolean) => { + setGuestsTCState(onForGuests) + applyClientTrackChangesStateToServer( + trackChangesOnForEveryone, + onForGuests, + trackChangesState + ) + }, + [ + applyClientTrackChangesStateToServer, + setGuestsTCState, + trackChangesOnForEveryone, + trackChangesState, + ] + ) + + const toggleTrackChangesForUser = useCallback< + ReviewPanel.UpdaterFn<'toggleTrackChangesForUser'> + >( + (onForUser: boolean, userId: UserId) => { + const newTrackChangesState = setUserTCState( + trackChangesState, + userId, + onForUser, + true + ) + applyClientTrackChangesStateToServer( + trackChangesOnForEveryone, + trackChangesOnForGuests, + newTrackChangesState + ) + }, + [ + applyClientTrackChangesStateToServer, + setUserTCState, + trackChangesOnForEveryone, + trackChangesOnForGuests, + trackChangesState, + ] + ) + + const applyTrackChangesStateToClient = useCallback( + (state: boolean | Record) => { + if (typeof state === 'boolean') { + setEveryoneTCState(state) + setGuestsTCState(state) + } else { + setTrackChangesOnForEveryone(false) + // TODO + // @ts-ignore + setGuestsTCState(state.__guests__ === true) + + let newTrackChangesState: ReviewPanel.Value<'trackChangesState'> = { + ...trackChangesState, + } + for (const member of project.members as any[]) { + newTrackChangesState = setUserTCState( + newTrackChangesState, + member._id, + state[member._id] ?? false + ) + } + newTrackChangesState = setUserTCState( + newTrackChangesState, + project.owner._id, + state[project.owner._id] ?? false + ) + return newTrackChangesState + } + }, + [ + project.members, + project.owner._id, + setEveryoneTCState, + setGuestsTCState, + setUserTCState, + trackChangesState, + ] + ) + + const setGuestFeatureBasedOnProjectAccessLevel = ( + projectPublicAccessLevel: PublicAccessLevel + ) => { + setTrackChangesForGuestsAvailable(projectPublicAccessLevel === 'tokenBased') + } + + useEffect(() => { + setGuestFeatureBasedOnProjectAccessLevel(project.publicAccessLevel) + }, [project.publicAccessLevel]) + + useEffect(() => { + if ( + trackChangesForGuestsAvailable || + !trackChangesOnForGuests || + trackChangesOnForEveryone + ) { + return + } + + // Overrides guest setting + toggleTrackChangesForGuests(false) + }, [ + toggleTrackChangesForGuests, + trackChangesForGuestsAvailable, + trackChangesOnForEveryone, + trackChangesOnForGuests, + ]) + + const projectJoinedEffectExecuted = useRef(false) + useEffect(() => { + if (!projectJoinedEffectExecuted.current) { + requestAnimationFrame(() => { + if (trackChanges) { + applyTrackChangesStateToClient(project.trackChangesState) + } else { + applyTrackChangesStateToClient(false) + } + setGuestFeatureBasedOnProjectAccessLevel(project.publicAccessLevel) + }) + projectJoinedEffectExecuted.current = true + } + }, [ + applyTrackChangesStateToClient, + trackChanges, + project.publicAccessLevel, + project.trackChangesState, + ]) + + useEffect(() => { + setFormattedProjectMembers(prevState => { + const tempFormattedProjectMembers: typeof prevState = {} + if (project.owner) { + tempFormattedProjectMembers[project.owner._id] = formatUser( + project.owner + ) + } + if (project.members) { + for (const member of project.members) { + if (member.privileges === 'readAndWrite') { + if (!trackChangesState[member._id]) { + // An added member will have track changes enabled if track changes is on for everyone + setUserTCState( + trackChangesState, + member._id, + trackChangesOnForEveryone, + true + ) + } + tempFormattedProjectMembers[member._id] = formatUser(member) + } + } + } + return tempFormattedProjectMembers + }) + }, [ + project.members, + project.owner, + setUserTCState, + trackChangesOnForEveryone, + trackChangesState, + ]) + + useSocketListener( + socket, + 'toggle-track-changes', + applyTrackChangesStateToClient + ) + const [resolveComment] = useScopeValue>('resolveComment') const [submitNewComment] = @@ -95,10 +459,6 @@ function useReviewPanelState(): ReviewPanelStateReactIde { (entry: { thread_id: ThreadId; replyContent: string }) => void >('submitReply') - const [formattedProjectMembers] = useScopeValue< - ReviewPanel.Value<'formattedProjectMembers'> - >('reviewPanel.formattedProjectMembers') - const toggleReviewPanel = useCallback(() => { if (!trackChangesVisible) { return @@ -150,6 +510,43 @@ function useReviewPanelState(): ReviewPanelStateReactIde { const [toolbarHeight, setToolbarHeight] = useState(0) const [layoutSuspended, setLayoutSuspended] = useState(false) + // listen for events from the CodeMirror 6 track changes extension + useEffect(() => { + const toggleTrackChangesFromKbdShortcut = () => { + if (trackChangesVisible && trackChanges) { + const userId: UserId = user.id + const state = trackChangesState[userId] + if (state) { + toggleTrackChangesForUser(!state.value, userId) + } + } + } + + const handleEditorEvents = (e: Event) => { + const event = e as CustomEvent + const { type } = event.detail + + switch (type) { + case 'toggle-track-changes': { + toggleTrackChangesFromKbdShortcut() + break + } + } + } + + window.addEventListener('editor:event', handleEditorEvents) + + return () => { + window.removeEventListener('editor:event', handleEditorEvents) + } + }, [ + toggleTrackChangesForUser, + trackChanges, + trackChangesState, + trackChangesVisible, + user.id, + ]) + const values = useMemo( () => ({ collapsed, diff --git a/services/web/frontend/js/features/ide-react/context/review-panel/review-panel-context.tsx b/services/web/frontend/js/features/ide-react/context/review-panel/review-panel-context.tsx index 3187d93e6f..0c40aa036f 100644 --- a/services/web/frontend/js/features/ide-react/context/review-panel/review-panel-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/review-panel/review-panel-context.tsx @@ -1,4 +1,4 @@ -import { useContext, createContext } from 'react' +import { createContext } from 'react' import useReviewPanelState from '@/features/ide-react/context/review-panel/hooks/use-review-panel-state' import { ReviewPanelStateReactIde } from '@/features/ide-react/context/review-panel/types/review-panel-state' @@ -21,23 +21,3 @@ export const ReviewPanelReactIdeProvider: React.FC = ({ children }) => { ) } - -export function useReviewPanelReactIdeValueContext() { - const context = useContext(ReviewPanelReactIdeValueContext) - if (!context) { - throw new Error( - 'ReviewPanelReactIdeValueContext is only available inside ReviewPanelReactIdeProvider' - ) - } - return context -} - -export function useReviewPanelReactIdeUpdaterFnsContext() { - const context = useContext(ReviewPanelReactIdeUpdaterFnsContext) - if (!context) { - throw new Error( - 'ReviewPanelReactIdeUpdaterFnsContext is only available inside ReviewPanelReactIdeProvider' - ) - } - return context -} diff --git a/services/web/frontend/js/features/ide-react/scope-adapters/editor-manager-context-adapter.ts b/services/web/frontend/js/features/ide-react/scope-adapters/editor-manager-context-adapter.ts new file mode 100644 index 0000000000..7b2f49a6ad --- /dev/null +++ b/services/web/frontend/js/features/ide-react/scope-adapters/editor-manager-context-adapter.ts @@ -0,0 +1,30 @@ +import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store' + +export function populateEditorScope( + store: ReactScopeValueStore, + projectId: string +) { + // This value is not used in the React code. It's just here to prevent errors + // from EditorProvider + store.set('state.loading', false) + + store.set('project.name', null) + + store.set('editor', { + showSymbolPalette: false, + toggleSymbolPalette: () => {}, + sharejs_doc: null, + open_doc_id: null, + open_doc_name: null, + opening: true, + trackChanges: false, + wantTrackChanges: false, + // No Ace here + newSourceEditor: true, + error_state: false, + }) + store.persisted('editor.showVisual', false, `editor.mode.${projectId}`, { + toPersisted: showVisual => (showVisual ? 'rich-text' : 'source'), + fromPersisted: mode => mode === 'rich-text', + }) +} diff --git a/services/web/frontend/js/features/ide-react/scope-adapters/review-panel-context-adapter.ts b/services/web/frontend/js/features/ide-react/scope-adapters/review-panel-context-adapter.ts index ca3c43606a..72990f3a02 100644 --- a/services/web/frontend/js/features/ide-react/scope-adapters/review-panel-context-adapter.ts +++ b/services/web/frontend/js/features/ide-react/scope-adapters/review-panel-context-adapter.ts @@ -17,20 +17,11 @@ export default function populateReviewPanelScope(store: ReactScopeValueStore) { store.set('users', {}) store.set('reviewPanel.resolvedComments', {}) store.set('reviewPanel.rendererData.lineHeight', 0) - store.set('reviewPanel.trackChangesState', {}) - store.set('reviewPanel.trackChangesOnForEveryone', false) - store.set('reviewPanel.trackChangesOnForGuests', false) - store.set('reviewPanel.trackChangesForGuestsAvailable', false) - store.set('reviewPanel.formattedProjectMembers', {}) - store.set('toggleTrackChangesForEveryone', () => {}) - store.set('toggleTrackChangesForUser', () => {}) - store.set('toggleTrackChangesForGuests', () => {}) store.set('resolveComment', () => {}) store.set('submitNewComment', async () => {}) store.set('deleteComment', () => {}) store.set('gotoEntry', () => {}) store.set('saveEdit', () => {}) - store.set('toggleReviewPanel', () => {}) store.set('unresolveComment', () => {}) store.set('deleteThread', () => {}) store.set('refreshResolvedCommentsDropdown', async () => {}) diff --git a/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toggle-menu.tsx b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toggle-menu.tsx index c3b89d5552..31c4305002 100644 --- a/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toggle-menu.tsx +++ b/services/web/frontend/js/features/source-editor/components/review-panel/toolbar/toggle-menu.tsx @@ -141,11 +141,11 @@ function ToggleMenu() { description={t('track_changes_for_x', { name: member.name })} handleToggle={() => toggleTrackChangesForUser( - !trackChangesState[member.id].value, + !trackChangesState[member.id]?.value, member.id ) } - value={trackChangesState[member.id].value} + value={Boolean(trackChangesState[member.id]?.value)} disabled={ trackChangesOnForEveryone || !project.features.trackChanges || 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 2c2b9d05dc..b9a7b315b2 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 @@ -9,6 +9,7 @@ import { } from '../../../../../../../types/review-panel/review-panel' import { DocId } from '../../../../../../../types/project-settings' import { dispatchReviewPanelLayout } from '../../../extensions/changes/change-manager' +import { UserId } from '../../../../../../../types/user' /* eslint-disable no-use-before-define */ export interface ReviewPanelState { @@ -31,14 +32,16 @@ export interface ReviewPanelState { loading: boolean openDocId: DocId | null lineHeight: number - trackChangesState: Record + trackChangesState: + | Record + | Record trackChangesOnForEveryone: boolean trackChangesOnForGuests: boolean trackChangesForGuestsAvailable: boolean formattedProjectMembers: Record< string, { - id: string + id: UserId name: string } > @@ -55,9 +58,9 @@ export interface ReviewPanelState { submitReply: (threadId: ThreadId, replyContent: string) => void acceptChanges: (entryIds: unknown) => void rejectChanges: (entryIds: unknown) => void - toggleTrackChangesForEveryone: (isOn: boolean) => unknown - toggleTrackChangesForUser: (isOn: boolean, memberId: string) => unknown - toggleTrackChangesForGuests: (isOn: boolean) => unknown + toggleTrackChangesForEveryone: (onForEveryone: boolean) => void + toggleTrackChangesForUser: (onForUser: boolean, userId: UserId) => void + toggleTrackChangesForGuests: (onForGuests: boolean) => void toggleReviewPanel: () => void bulkAcceptActions: () => void bulkRejectActions: () => void diff --git a/services/web/frontend/js/shared/context/project-context.jsx b/services/web/frontend/js/shared/context/project-context.jsx index ad599955cc..654086bf01 100644 --- a/services/web/frontend/js/shared/context/project-context.jsx +++ b/services/web/frontend/js/shared/context/project-context.jsx @@ -89,6 +89,7 @@ export function ProjectProvider({ children }) { publicAccesLevel: publicAccessLevel, owner, showNewCompileTimeoutUI, + trackChangesState, } = project || projectFallback const tags = useMemo( @@ -123,6 +124,7 @@ export function ProjectProvider({ children }) { showNewCompileTimeoutUI: newCompileTimeoutOverride || showNewCompileTimeoutUI, tags, + trackChangesState, } }, [ _id, @@ -136,6 +138,7 @@ export function ProjectProvider({ children }) { showNewCompileTimeoutUI, newCompileTimeoutOverride, tags, + trackChangesState, ]) return ( diff --git a/services/web/frontend/stories/decorators/scope.tsx b/services/web/frontend/stories/decorators/scope.tsx index 2dd5773f01..8e3b3bfaae 100644 --- a/services/web/frontend/stories/decorators/scope.tsx +++ b/services/web/frontend/stories/decorators/scope.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo } from 'react' import { get } from 'lodash' -import { User } from '../../../types/user' +import { User, UserId } from '../../../types/user' import { Project } from '../../../types/project' import { mockBuildFile, @@ -26,7 +26,7 @@ const scopeWatchers: [string, (value: any) => void][] = [] const initialize = () => { const user: User = { - id: 'story-user', + id: 'story-user' as UserId, email: 'story-user@example.com', allowedFreeTrial: true, features: { dropbox: true, symbolPalette: true }, diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-autocomplete.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-autocomplete.spec.tsx index 5410617d54..654ecb7ba1 100644 --- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-autocomplete.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-autocomplete.spec.tsx @@ -6,6 +6,7 @@ import { mockScope } from '../helpers/mock-scope' import { EditorProviders } from '../../../helpers/editor-providers' import CodeMirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor' import { activeEditorLine } from '../helpers/active-editor-line' +import { UserId } from '../../../../../types/user' const Container: FC = ({ children }) => (
{children}
@@ -860,7 +861,7 @@ describe('autocomplete', { scrollBehavior: false }, function () { window.metaAttributesCache.set('ol-showSymbolPalette', true) const user = { - id: '123abd', + id: '123abd' as UserId, email: 'testuser@example.com', } cy.mount( diff --git a/services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx index bb98b081c0..de0a52b8f2 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx @@ -16,6 +16,7 @@ import { groupActiveSubscriptionWithPendingLicenseChange, } from '../../fixtures/subscriptions' import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location' +import { UserId } from '../../../../../../types/user' const userId = 'fff999fff999' const memberGroupSubscriptions: MemberGroupSubscription[] = [ @@ -24,7 +25,7 @@ const memberGroupSubscriptions: MemberGroupSubscription[] = [ userIsGroupManager: false, planLevelName: 'Professional', admin_id: { - id: 'abc123abc123', + id: 'abc123abc123' as UserId, email: 'you@example.com', }, }, @@ -33,7 +34,7 @@ const memberGroupSubscriptions: MemberGroupSubscription[] = [ userIsGroupManager: true, planLevelName: 'Collaborator', admin_id: { - id: 'bcd456bcd456', + id: 'bcd456bcd456' as UserId, email: 'someone@example.com', }, }, diff --git a/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx index 9f4868b028..126fb0169a 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/managed-group-subscriptions.test.tsx @@ -10,6 +10,7 @@ import { cleanUpContext, renderWithSubscriptionDashContext, } from '../../helpers/render-with-subscription-dash-context' +import { UserId } from '../../../../../../types/user' function getManagedGroupSubscription(groupSSO: boolean, managedUsers: boolean) { const subscriptionOne = { @@ -17,7 +18,7 @@ function getManagedGroupSubscription(groupSSO: boolean, managedUsers: boolean) { userIsGroupMember: true, planLevelName: 'Professional', admin_id: { - id: 'abc123abc123', + id: 'abc123abc123' as UserId, email: 'you@example.com', }, features: { @@ -31,7 +32,7 @@ function getManagedGroupSubscription(groupSSO: boolean, managedUsers: boolean) { userIsGroupMember: false, planLevelName: 'Collaborator', admin_id: { - id: 'bcd456bcd456', + id: 'bcd456bcd456' as UserId, email: 'someone@example.com', }, features: { diff --git a/services/web/types/public-access-level.ts b/services/web/types/public-access-level.ts new file mode 100644 index 0000000000..22c6a75dbf --- /dev/null +++ b/services/web/types/public-access-level.ts @@ -0,0 +1,5 @@ +export type PublicAccessLevel = + | 'readOnly' + | 'readAndWrite' + | 'private' + | 'tokenBased' diff --git a/services/web/types/review-panel/comment-thread.ts b/services/web/types/review-panel/comment-thread.ts index 8b10fed3c7..1df267a476 100644 --- a/services/web/types/review-panel/comment-thread.ts +++ b/services/web/types/review-panel/comment-thread.ts @@ -1,8 +1,8 @@ import { ReviewPanelCommentThreadMessage, ReviewPanelUser, - UserId, } from './review-panel' +import { UserId } from '../user' import { DateString } from '../helpers/date' interface ReviewPanelCommentThreadBase { diff --git a/services/web/types/review-panel/entry.ts b/services/web/types/review-panel/entry.ts index e4588ac6bd..803a0e062c 100644 --- a/services/web/types/review-panel/entry.ts +++ b/services/web/types/review-panel/entry.ts @@ -1,4 +1,5 @@ -import { ThreadId, UserId } from './review-panel' +import { ThreadId } from './review-panel' +import { UserId } from '../user' export interface ReviewPanelEntryScreenPos { y: number diff --git a/services/web/types/review-panel/review-panel.ts b/services/web/types/review-panel/review-panel.ts index 08c84776db..b6024fbfd3 100644 --- a/services/web/types/review-panel/review-panel.ts +++ b/services/web/types/review-panel/review-panel.ts @@ -1,5 +1,6 @@ import { Brand } from '../helpers/brand' import { DocId } from '../project-settings' +import { UserId } from '../user' import { ReviewPanelAddCommentEntry, ReviewPanelBulkActionsEntry, @@ -29,8 +30,6 @@ export type ReviewPanelDocEntries = Record< export type ReviewPanelEntries = Record -export type UserId = Brand - export interface ReviewPanelUser { avatar_text: string email: string diff --git a/services/web/types/user.ts b/services/web/types/user.ts index 2e5b6da088..e7d4718e83 100644 --- a/services/web/types/user.ts +++ b/services/web/types/user.ts @@ -1,10 +1,14 @@ +import { Brand } from './helpers/brand' + export type RefProviders = { mendeley?: boolean zotero?: boolean } +export type UserId = Brand + export type User = { - id: string + id: UserId email: string allowedFreeTrial?: boolean signUpDate?: string // date string