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
This commit is contained in:
ilkin-overleaf 2023-11-16 13:56:54 +02:00 committed by Copybot
parent 71a78c8edd
commit 7db5d761ea
18 changed files with 495 additions and 109 deletions

View file

@ -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<EditorManager | undefined>(undefined)
export const EditorManagerProvider: FC = ({ children }) => {

View file

@ -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'

View file

@ -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.Value<'subView'>>(
'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<ReviewPanel.Value<'trackChangesOnForEveryone'>>(false)
const [trackChangesOnForGuests, setTrackChangesOnForGuests] =
useState<ReviewPanel.Value<'trackChangesOnForGuests'>>(false)
const [trackChangesForGuestsAvailable, setTrackChangesForGuestsAvailable] =
useState<ReviewPanel.Value<'trackChangesForGuestsAvailable'>>(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<UserId, boolean>
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<ReviewPanel.Value<'trackChangesState'>>,
userId: UserId,
newValue: boolean,
isLocal = false
) => {
const newTrackChangesState: ReviewPanel.Value<'trackChangesState'> = {
...trackChangesState,
}
const state =
newTrackChangesState[userId] ??
({} as NonNullable<typeof newTrackChangesState[UserId]>)
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<UserId, boolean>) => {
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<ReviewPanel.UpdaterFn<'resolveComment'>>('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<ReviewPanelStateReactIde['values']>(
() => ({
collapsed,

View file

@ -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 }) => {
</ReviewPanelReactIdeValueContext.Provider>
)
}
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
}

View file

@ -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',
})
}

View file

@ -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 () => {})

View file

@ -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 ||

View file

@ -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<string, { value: boolean; syncState: string }>
trackChangesState:
| Record<UserId, { value: boolean; syncState: 'synced' | 'pending' }>
| Record<UserId, undefined>
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

View file

@ -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 (

View file

@ -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 },

View file

@ -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 }) => (
<div style={{ width: 785, height: 785 }}>{children}</div>
@ -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(

View file

@ -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',
},
},

View file

@ -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: {

View file

@ -0,0 +1,5 @@
export type PublicAccessLevel =
| 'readOnly'
| 'readAndWrite'
| 'private'
| 'tokenBased'

View file

@ -1,8 +1,8 @@
import {
ReviewPanelCommentThreadMessage,
ReviewPanelUser,
UserId,
} from './review-panel'
import { UserId } from '../user'
import { DateString } from '../helpers/date'
interface ReviewPanelCommentThreadBase {

View file

@ -1,4 +1,5 @@
import { ThreadId, UserId } from './review-panel'
import { ThreadId } from './review-panel'
import { UserId } from '../user'
export interface ReviewPanelEntryScreenPos {
y: number

View file

@ -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<DocId, ReviewPanelDocEntries>
export type UserId = Brand<string, 'UserId'>
export interface ReviewPanelUser {
avatar_text: string
email: string

View file

@ -1,10 +1,14 @@
import { Brand } from './helpers/brand'
export type RefProviders = {
mendeley?: boolean
zotero?: boolean
}
export type UserId = Brand<string, 'UserId'>
export type User = {
id: string
id: UserId
email: string
allowedFreeTrial?: boolean
signUpDate?: string // date string