mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #14709 from overleaf/ae-context-typescript
Convert React context providers to TypeScript [don't squash!] GitOrigin-RevId: d92a91798286978410956ab791d73c17c5086d86
This commit is contained in:
parent
fdf8ebe001
commit
0cde5be165
60 changed files with 1146 additions and 1080 deletions
|
@ -1,5 +1,4 @@
|
|||
import React, { lazy, Suspense, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import MessageInput from './message-input'
|
||||
|
@ -19,9 +18,7 @@ const ChatPane = React.memo(function ChatPane() {
|
|||
const { t } = useTranslation()
|
||||
|
||||
const { chatIsOpen } = useLayoutContext()
|
||||
const user = useUserContext({
|
||||
id: PropTypes.string.isRequired,
|
||||
})
|
||||
const user = useUserContext()
|
||||
|
||||
const {
|
||||
status,
|
|
@ -6,8 +6,8 @@ import {
|
|||
useReducer,
|
||||
useMemo,
|
||||
useRef,
|
||||
FC,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
|
@ -20,7 +20,55 @@ import { useIdeContext } from '@/shared/context/ide-context'
|
|||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
export function chatReducer(state, action) {
|
||||
export type Message = {
|
||||
id: string
|
||||
timestamp: number
|
||||
contents: string
|
||||
}
|
||||
|
||||
type State = {
|
||||
status: 'idle' | 'pending' | 'error'
|
||||
messages: Message[]
|
||||
initialMessagesLoaded: boolean
|
||||
lastTimestamp: number | null
|
||||
atEnd: boolean
|
||||
unreadMessageCount: number
|
||||
error?: Error | null
|
||||
uniqueMessageIds: string[]
|
||||
}
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: 'INITIAL_FETCH_MESSAGES'
|
||||
}
|
||||
| {
|
||||
type: 'FETCH_MESSAGES'
|
||||
}
|
||||
| {
|
||||
type: 'FETCH_MESSAGES_SUCCESS'
|
||||
messages: Message[]
|
||||
}
|
||||
| {
|
||||
type: 'SEND_MESSAGE'
|
||||
user: any
|
||||
content: any
|
||||
}
|
||||
| {
|
||||
type: 'RECEIVE_MESSAGE'
|
||||
message: any
|
||||
}
|
||||
| {
|
||||
type: 'MARK_MESSAGES_AS_READ'
|
||||
}
|
||||
| {
|
||||
type: 'CLEAR'
|
||||
}
|
||||
| {
|
||||
type: 'ERROR'
|
||||
error: any
|
||||
}
|
||||
|
||||
function chatReducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'INITIAL_FETCH_MESSAGES':
|
||||
return {
|
||||
|
@ -99,7 +147,7 @@ export function chatReducer(state, action) {
|
|||
}
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
const initialState: State = {
|
||||
status: 'idle',
|
||||
messages: [],
|
||||
initialMessagesLoaded: false,
|
||||
|
@ -110,32 +158,27 @@ const initialState = {
|
|||
uniqueMessageIds: [],
|
||||
}
|
||||
|
||||
export const ChatContext = createContext()
|
||||
export const ChatContext = createContext<
|
||||
| {
|
||||
status: 'idle' | 'pending' | 'error'
|
||||
messages: Message[]
|
||||
initialMessagesLoaded: boolean
|
||||
atEnd: boolean
|
||||
unreadMessageCount: number
|
||||
loadInitialMessages: () => void
|
||||
loadMoreMessages: () => void
|
||||
sendMessage: (message: any) => void
|
||||
markMessagesAsRead: () => void
|
||||
reset: () => void
|
||||
error?: Error | null
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
ChatContext.Provider.propTypes = {
|
||||
value: PropTypes.shape({
|
||||
status: PropTypes.string.isRequired,
|
||||
messages: PropTypes.array.isRequired,
|
||||
initialMessagesLoaded: PropTypes.bool.isRequired,
|
||||
atEnd: PropTypes.bool.isRequired,
|
||||
unreadMessageCount: PropTypes.number.isRequired,
|
||||
loadInitialMessages: PropTypes.func.isRequired,
|
||||
loadMoreMessages: PropTypes.func.isRequired,
|
||||
sendMessage: PropTypes.func.isRequired,
|
||||
markMessagesAsRead: PropTypes.func.isRequired,
|
||||
reset: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
export function ChatProvider({ children }) {
|
||||
export const ChatProvider: FC = ({ children }) => {
|
||||
const clientId = useRef(uuid())
|
||||
const user = useUserContext({
|
||||
id: PropTypes.string.isRequired,
|
||||
})
|
||||
const { _id: projectId } = useProjectContext({
|
||||
_id: PropTypes.string.isRequired,
|
||||
})
|
||||
const user = useUserContext()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
const { chatIsOpen } = useLayoutContext()
|
||||
|
||||
|
@ -151,10 +194,12 @@ export function ChatProvider({ children }) {
|
|||
function fetchMessages() {
|
||||
if (state.atEnd) return
|
||||
|
||||
const query = { limit: PAGE_SIZE }
|
||||
const query: Record<string, string> = {
|
||||
limit: String(PAGE_SIZE),
|
||||
}
|
||||
|
||||
if (state.lastTimestamp) {
|
||||
query.before = state.lastTimestamp
|
||||
query.before = String(state.lastTimestamp)
|
||||
}
|
||||
|
||||
const queryString = new URLSearchParams(query)
|
||||
|
@ -231,7 +276,7 @@ export function ChatProvider({ children }) {
|
|||
useEffect(() => {
|
||||
if (!socket) return
|
||||
|
||||
function receivedMessage(message) {
|
||||
function receivedMessage(message: any) {
|
||||
// If the message is from the current client id, then we are receiving the sent message back from the socket.
|
||||
// Ignore it to prevent double message.
|
||||
if (message.clientId === clientId.current) return
|
||||
|
@ -299,12 +344,10 @@ export function ChatProvider({ children }) {
|
|||
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>
|
||||
}
|
||||
|
||||
ChatProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
export function useChatContext(propTypes) {
|
||||
const data = useContext(ChatContext)
|
||||
PropTypes.checkPropTypes(propTypes, data, 'data', 'ChatContext.Provider')
|
||||
return data
|
||||
export function useChatContext() {
|
||||
const context = useContext(ChatContext)
|
||||
if (!context) {
|
||||
throw new Error('useChatContext is only available inside ChatProvider')
|
||||
}
|
||||
return context
|
||||
}
|
|
@ -6,12 +6,11 @@ import { useProjectSettingsContext } from '../../context/project-settings-contex
|
|||
import SettingsMenuSelect from './settings-menu-select'
|
||||
import type { Option } from './settings-menu-select'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
import { MainDocument } from '../../../../../../types/project-settings'
|
||||
|
||||
export default function SettingsDocument() {
|
||||
const { t } = useTranslation()
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
const docs: MainDocument[] = useFileTreeData().docs
|
||||
const { docs } = useFileTreeData()
|
||||
const { rootDocId, setRootDocId } = useProjectSettingsContext()
|
||||
|
||||
const validDocsOptions = useMemo(() => {
|
||||
|
|
|
@ -1,34 +1,13 @@
|
|||
import React, { useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ToolbarHeader from './toolbar-header'
|
||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||
import { useChatContext } from '../../chat/context/chat-context'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import { Doc } from '../../../../../types/doc'
|
||||
|
||||
const projectContextPropTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
features: PropTypes.shape({
|
||||
trackChangesVisible: PropTypes.bool,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
const editorContextPropTypes = {
|
||||
cobranding: PropTypes.object,
|
||||
loading: PropTypes.bool,
|
||||
isRestrictedTokenMember: PropTypes.bool,
|
||||
renameProject: PropTypes.func.isRequired,
|
||||
isProjectOwner: PropTypes.bool,
|
||||
permissionsLevel: PropTypes.string,
|
||||
}
|
||||
|
||||
const chatContextPropTypes = {
|
||||
markMessagesAsRead: PropTypes.func.isRequired,
|
||||
unreadMessageCount: PropTypes.number.isRequired,
|
||||
}
|
||||
|
||||
function isOpentoString(open) {
|
||||
function isOpentoString(open: boolean) {
|
||||
return open ? 'open' : 'close'
|
||||
}
|
||||
|
||||
|
@ -37,19 +16,22 @@ const EditorNavigationToolbarRoot = React.memo(
|
|||
onlineUsersArray,
|
||||
openDoc,
|
||||
openShareProjectModal,
|
||||
}: {
|
||||
onlineUsersArray: any[]
|
||||
openDoc: (doc: Doc, { gotoLine }: { gotoLine: number }) => void
|
||||
openShareProjectModal: () => void
|
||||
}) {
|
||||
const {
|
||||
name: projectName,
|
||||
features: { trackChangesVisible },
|
||||
} = useProjectContext(projectContextPropTypes)
|
||||
} = useProjectContext()
|
||||
|
||||
const {
|
||||
cobranding,
|
||||
loading,
|
||||
isRestrictedTokenMember,
|
||||
renameProject,
|
||||
permissionsLevel,
|
||||
} = useEditorContext(editorContextPropTypes)
|
||||
} = useEditorContext()
|
||||
|
||||
const {
|
||||
chatIsOpen,
|
||||
|
@ -61,8 +43,7 @@ const EditorNavigationToolbarRoot = React.memo(
|
|||
setLeftMenuShown,
|
||||
} = useLayoutContext()
|
||||
|
||||
const { markMessagesAsRead, unreadMessageCount } =
|
||||
useChatContext(chatContextPropTypes)
|
||||
const { markMessagesAsRead, unreadMessageCount } = useChatContext()
|
||||
|
||||
const toggleChatOpen = useCallback(() => {
|
||||
if (!chatIsOpen) {
|
||||
|
@ -110,11 +91,9 @@ const EditorNavigationToolbarRoot = React.memo(
|
|||
[openDoc]
|
||||
)
|
||||
|
||||
// using {display: 'none'} as 1:1 migration from Angular's ng-hide. Using
|
||||
// `loading ? null : <ToolbarHeader/>` causes UI glitches
|
||||
return (
|
||||
<ToolbarHeader
|
||||
style={loading ? { display: 'none' } : {}}
|
||||
// @ts-ignore: TODO(convert ToolbarHeader to TSX)
|
||||
cobranding={cobranding}
|
||||
onShowLeftMenuClick={onShowLeftMenuClick}
|
||||
chatIsOpen={chatIsOpen}
|
||||
|
@ -140,10 +119,4 @@ const EditorNavigationToolbarRoot = React.memo(
|
|||
}
|
||||
)
|
||||
|
||||
EditorNavigationToolbarRoot.propTypes = {
|
||||
onlineUsersArray: PropTypes.array.isRequired,
|
||||
openDoc: PropTypes.func.isRequired,
|
||||
openShareProjectModal: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default EditorNavigationToolbarRoot
|
|
@ -1,14 +1,18 @@
|
|||
import { memo, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Dropdown, MenuItem } from 'react-bootstrap'
|
||||
import { memo, ReactNode, useCallback } from 'react'
|
||||
import { Dropdown, MenuItem, MenuItemProps } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Tooltip from '../../../shared/components/tooltip'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import IconChecked from '../../../shared/components/icon-checked'
|
||||
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import {
|
||||
IdeLayout,
|
||||
IdeView,
|
||||
useLayoutContext,
|
||||
} from '../../../shared/context/layout-context'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import useEventListener from '../../../shared/hooks/use-event-listener'
|
||||
import { DetachRole } from '@/shared/context/detach-context'
|
||||
|
||||
function IconPlaceholder() {
|
||||
return <Icon type="" fw />
|
||||
|
@ -38,7 +42,17 @@ function IconPdfOnly() {
|
|||
return <Icon type="file-pdf-o" fw />
|
||||
}
|
||||
|
||||
function IconCheckmark({ iconFor, pdfLayout, view, detachRole }) {
|
||||
function IconCheckmark({
|
||||
iconFor,
|
||||
pdfLayout,
|
||||
view,
|
||||
detachRole,
|
||||
}: {
|
||||
iconFor: string
|
||||
pdfLayout: IdeLayout
|
||||
view: IdeView | null
|
||||
detachRole?: DetachRole
|
||||
}) {
|
||||
if (detachRole === 'detacher' || view === 'history') {
|
||||
return <IconPlaceholder />
|
||||
}
|
||||
|
@ -57,7 +71,16 @@ function IconCheckmark({ iconFor, pdfLayout, view, detachRole }) {
|
|||
return <IconPlaceholder />
|
||||
}
|
||||
|
||||
function LayoutMenuItem({ checkmark, icon, text, ...props }) {
|
||||
function LayoutMenuItem({
|
||||
checkmark,
|
||||
icon,
|
||||
text,
|
||||
...props
|
||||
}: {
|
||||
checkmark: ReactNode
|
||||
icon: ReactNode
|
||||
text: string | ReactNode
|
||||
} & MenuItemProps) {
|
||||
return (
|
||||
<MenuItem {...props}>
|
||||
<div className="layout-menu-item">
|
||||
|
@ -70,12 +93,6 @@ function LayoutMenuItem({ checkmark, icon, text, ...props }) {
|
|||
</MenuItem>
|
||||
)
|
||||
}
|
||||
LayoutMenuItem.propTypes = {
|
||||
checkmark: PropTypes.node.isRequired,
|
||||
icon: PropTypes.node.isRequired,
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||
onSelect: PropTypes.func,
|
||||
}
|
||||
|
||||
function DetachDisabled() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -126,7 +143,7 @@ function LayoutDropdownButton() {
|
|||
useEventListener('ui:pdf-open', handleReattach)
|
||||
|
||||
const handleChangeLayout = useCallback(
|
||||
(newLayout, newView) => {
|
||||
(newLayout: IdeLayout, newView?: IdeView) => {
|
||||
handleReattach()
|
||||
changeLayout(newLayout, newView)
|
||||
eventTracking.sendMB('project-layout-change', {
|
||||
|
@ -243,10 +260,3 @@ function LayoutDropdownButton() {
|
|||
}
|
||||
|
||||
export default memo(LayoutDropdownButton)
|
||||
|
||||
IconCheckmark.propTypes = {
|
||||
iconFor: PropTypes.string.isRequired,
|
||||
pdfLayout: PropTypes.string.isRequired,
|
||||
view: PropTypes.string,
|
||||
detachRole: PropTypes.string,
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||
import { useFileTreeMainContext } from '../contexts/file-tree-main'
|
||||
|
@ -9,7 +7,7 @@ import { useFileTreeMainContext } from '../contexts/file-tree-main'
|
|||
import FileTreeItemMenuItems from './file-tree-item/file-tree-item-menu-items'
|
||||
|
||||
function FileTreeContextMenu() {
|
||||
const { permissionsLevel } = useEditorContext(editorContextPropTypes)
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
|
||||
|
||||
if (permissionsLevel === 'readOnly' || !contextMenuCoords) return null
|
||||
|
@ -19,7 +17,7 @@ function FileTreeContextMenu() {
|
|||
setContextMenuCoords(null)
|
||||
}
|
||||
|
||||
function handleToggle(wantOpen) {
|
||||
function handleToggle(wantOpen: boolean) {
|
||||
if (!wantOpen) close()
|
||||
}
|
||||
|
||||
|
@ -39,19 +37,17 @@ function FileTreeContextMenu() {
|
|||
<FileTreeItemMenuItems />
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>,
|
||||
document.querySelector('body')
|
||||
document.body
|
||||
)
|
||||
}
|
||||
|
||||
const editorContextPropTypes = {
|
||||
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
|
||||
}
|
||||
|
||||
// fake component required as Dropdowns require a Toggle, even tho we don't want
|
||||
// one for the context menu
|
||||
const FakeDropDownToggle = React.forwardRef((props, ref) => {
|
||||
return null
|
||||
})
|
||||
const FakeDropDownToggle = React.forwardRef<undefined, { bsRole: string }>(
|
||||
({ bsRole }, ref) => {
|
||||
return null
|
||||
}
|
||||
)
|
||||
|
||||
FakeDropDownToggle.displayName = 'FakeDropDownToggle'
|
||||
|
|
@ -1,23 +1,28 @@
|
|||
import PropTypes from 'prop-types'
|
||||
|
||||
import { FileTreeMainProvider } from '../contexts/file-tree-main'
|
||||
import { FileTreeActionableProvider } from '../contexts/file-tree-actionable'
|
||||
import { FileTreeSelectableProvider } from '../contexts/file-tree-selectable'
|
||||
import { FileTreeDraggableProvider } from '../contexts/file-tree-draggable'
|
||||
import { FC } from 'react'
|
||||
|
||||
// renders all the contexts needed for the file tree:
|
||||
// FileTreeMain: generic store
|
||||
// FileTreeActionable: global UI state for actions (rename, delete, etc.)
|
||||
// FileTreeMutable: provides entities mutation operations
|
||||
// FileTreeSelectable: handles selection and multi-selection
|
||||
function FileTreeContext({
|
||||
const FileTreeContext: FC<{
|
||||
reindexReferences: () => void
|
||||
refProviders: Record<string, boolean>
|
||||
setRefProviderEnabled: (provider: string, value: boolean) => void
|
||||
setStartedFreeTrial: (value: boolean) => void
|
||||
onSelect: () => void
|
||||
}> = ({
|
||||
refProviders,
|
||||
reindexReferences,
|
||||
setRefProviderEnabled,
|
||||
setStartedFreeTrial,
|
||||
onSelect,
|
||||
children,
|
||||
}) {
|
||||
}) => {
|
||||
return (
|
||||
<FileTreeMainProvider
|
||||
refProviders={refProviders}
|
||||
|
@ -34,16 +39,4 @@ function FileTreeContext({
|
|||
)
|
||||
}
|
||||
|
||||
FileTreeContext.propTypes = {
|
||||
reindexReferences: PropTypes.func.isRequired,
|
||||
refProviders: PropTypes.object.isRequired,
|
||||
setRefProviderEnabled: PropTypes.func.isRequired,
|
||||
setStartedFreeTrial: PropTypes.func.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
}
|
||||
|
||||
export default FileTreeContext
|
|
@ -1,11 +1,16 @@
|
|||
import { useState, useCallback, useEffect, useMemo } from 'react'
|
||||
import {
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
FormEventHandler,
|
||||
} from 'react'
|
||||
import { Button, ControlLabel, FormControl, FormGroup } from 'react-bootstrap'
|
||||
import Icon from '../../../../../shared/components/icon'
|
||||
import FileTreeCreateNameInput from '../file-tree-create-name-input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useUserProjects } from '../../../hooks/use-user-projects'
|
||||
import { useProjectEntities } from '../../../hooks/use-project-entities'
|
||||
import { Entity, useProjectEntities } from '../../../hooks/use-project-entities'
|
||||
import { useProjectOutputFiles } from '../../../hooks/use-project-output-files'
|
||||
import { useFileTreeActionable } from '../../../contexts/file-tree-actionable'
|
||||
import { useFileTreeCreateName } from '../../../contexts/file-tree-create-name'
|
||||
|
@ -13,6 +18,8 @@ import { useFileTreeCreateForm } from '../../../contexts/file-tree-create-form'
|
|||
import { useProjectContext } from '../../../../../shared/context/project-context'
|
||||
import ErrorMessage from '../error-message'
|
||||
import * as eventTracking from '../../../../../infrastructure/event-tracking'
|
||||
import { File } from '@/features/source-editor/utils/file'
|
||||
import { Project } from '../../../../../../../types/project'
|
||||
|
||||
export default function FileTreeImportFromProject() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -26,9 +33,11 @@ export default function FileTreeImportFromProject() {
|
|||
const { setValid } = useFileTreeCreateForm()
|
||||
const { error, finishCreatingLinkedFile } = useFileTreeActionable()
|
||||
|
||||
const [selectedProject, setSelectedProject] = useState()
|
||||
const [selectedProjectEntity, setSelectedProjectEntity] = useState()
|
||||
const [selectedProjectOutputFile, setSelectedProjectOutputFile] = useState()
|
||||
const [selectedProject, setSelectedProject] = useState<Project>()
|
||||
const [selectedProjectEntity, setSelectedProjectEntity] = useState<Entity>()
|
||||
const [selectedProjectOutputFile, setSelectedProjectOutputFile] = useState<
|
||||
File & { build: string; clsiServerId: string }
|
||||
>()
|
||||
const [isOutputFilesMode, setOutputFilesMode] = useState(
|
||||
// default to project file mode, unless the feature is not enabled
|
||||
!hasLinkedProjectFileFeature
|
||||
|
@ -51,10 +60,10 @@ export default function FileTreeImportFromProject() {
|
|||
if (selectedProjectOutputFile) {
|
||||
if (
|
||||
selectedProjectOutputFile.path === 'output.pdf' &&
|
||||
selectedProject.name
|
||||
selectedProject!.name
|
||||
) {
|
||||
// if the output PDF is selected, use the project's name as the filename
|
||||
setName(`${selectedProject.name}.pdf`)
|
||||
setName(`${selectedProject!.name}.pdf`)
|
||||
} else {
|
||||
setNameFromPath(selectedProjectOutputFile.path)
|
||||
}
|
||||
|
@ -84,7 +93,7 @@ export default function FileTreeImportFromProject() {
|
|||
])
|
||||
|
||||
// form submission: create a linked file with this name, from this entity or output file
|
||||
const handleSubmit = event => {
|
||||
const handleSubmit: FormEventHandler = event => {
|
||||
event.preventDefault()
|
||||
eventTracking.sendMB('new-file-created', { method: 'project' })
|
||||
|
||||
|
@ -93,10 +102,10 @@ export default function FileTreeImportFromProject() {
|
|||
name,
|
||||
provider: 'project_output_file',
|
||||
data: {
|
||||
source_project_id: selectedProject._id,
|
||||
source_output_file_path: selectedProjectOutputFile.path,
|
||||
build_id: selectedProjectOutputFile.build,
|
||||
clsiServerId: selectedProjectOutputFile.clsiServerId,
|
||||
source_project_id: selectedProject!._id,
|
||||
source_output_file_path: selectedProjectOutputFile!.path,
|
||||
build_id: selectedProjectOutputFile!.build,
|
||||
clsiServerId: selectedProjectOutputFile!.clsiServerId,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
|
@ -104,8 +113,8 @@ export default function FileTreeImportFromProject() {
|
|||
name,
|
||||
provider: 'project_file',
|
||||
data: {
|
||||
source_project_id: selectedProject._id,
|
||||
source_entity_path: selectedProjectEntity.path,
|
||||
source_project_id: selectedProject!._id,
|
||||
source_entity_path: selectedProjectEntity!.path,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -163,9 +172,17 @@ export default function FileTreeImportFromProject() {
|
|||
)
|
||||
}
|
||||
|
||||
function SelectProject({ selectedProject, setSelectedProject }) {
|
||||
type SelectProjectProps = {
|
||||
selectedProject?: any
|
||||
setSelectedProject(project: any): void
|
||||
}
|
||||
|
||||
function SelectProject({
|
||||
selectedProject,
|
||||
setSelectedProject,
|
||||
}: SelectProjectProps) {
|
||||
const { t } = useTranslation()
|
||||
const { _id: projectId } = useProjectContext(projectContextPropTypes)
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
const { data, error, loading } = useUserProjects()
|
||||
|
||||
|
@ -197,8 +214,8 @@ function SelectProject({ selectedProject, setSelectedProject }) {
|
|||
disabled={!data}
|
||||
value={selectedProject ? selectedProject._id : ''}
|
||||
onChange={event => {
|
||||
const projectId = event.target.value
|
||||
const project = data.find(item => item._id === projectId)
|
||||
const projectId = (event.target as HTMLSelectElement).value
|
||||
const project = data!.find(item => item._id === projectId)
|
||||
setSelectedProject(project)
|
||||
}}
|
||||
>
|
||||
|
@ -220,20 +237,18 @@ function SelectProject({ selectedProject, setSelectedProject }) {
|
|||
</FormGroup>
|
||||
)
|
||||
}
|
||||
SelectProject.propTypes = {
|
||||
selectedProject: PropTypes.object,
|
||||
setSelectedProject: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
const projectContextPropTypes = {
|
||||
_id: PropTypes.string.isRequired,
|
||||
type SelectProjectOutputFileProps = {
|
||||
selectedProjectId?: string
|
||||
selectedProjectOutputFile?: any
|
||||
setSelectedProjectOutputFile(file: any): void
|
||||
}
|
||||
|
||||
function SelectProjectOutputFile({
|
||||
selectedProjectId,
|
||||
selectedProjectOutputFile,
|
||||
setSelectedProjectOutputFile,
|
||||
}) {
|
||||
}: SelectProjectOutputFileProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data, error, loading } = useProjectOutputFiles(selectedProjectId)
|
||||
|
@ -261,8 +276,8 @@ function SelectProjectOutputFile({
|
|||
disabled={!data}
|
||||
value={selectedProjectOutputFile?.path || ''}
|
||||
onChange={event => {
|
||||
const path = event.target.value
|
||||
const file = data.find(item => item.path === path)
|
||||
const path = (event.target as HTMLSelectElement).value
|
||||
const file = data?.find(item => item.path === path)
|
||||
setSelectedProjectOutputFile(file)
|
||||
}}
|
||||
>
|
||||
|
@ -280,17 +295,18 @@ function SelectProjectOutputFile({
|
|||
</FormGroup>
|
||||
)
|
||||
}
|
||||
SelectProjectOutputFile.propTypes = {
|
||||
selectedProjectId: PropTypes.string,
|
||||
selectedProjectOutputFile: PropTypes.object,
|
||||
setSelectedProjectOutputFile: PropTypes.func.isRequired,
|
||||
|
||||
type SelectProjectEntityProps = {
|
||||
selectedProjectId?: string
|
||||
selectedProjectEntity?: any
|
||||
setSelectedProjectEntity(entity: any): void
|
||||
}
|
||||
|
||||
function SelectProjectEntity({
|
||||
selectedProjectId,
|
||||
selectedProjectEntity,
|
||||
setSelectedProjectEntity,
|
||||
}) {
|
||||
}: SelectProjectEntityProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data, error, loading } = useProjectEntities(selectedProjectId)
|
||||
|
@ -318,8 +334,8 @@ function SelectProjectEntity({
|
|||
disabled={!data}
|
||||
value={selectedProjectEntity?.path || ''}
|
||||
onChange={event => {
|
||||
const path = event.target.value
|
||||
const entity = data.find(item => item.path === path)
|
||||
const path = (event.target as HTMLSelectElement).value
|
||||
const entity = data!.find(item => item.path === path)
|
||||
setSelectedProjectEntity(entity)
|
||||
}}
|
||||
>
|
||||
|
@ -337,8 +353,3 @@ function SelectProjectEntity({
|
|||
</FormGroup>
|
||||
)
|
||||
}
|
||||
SelectProjectEntity.propTypes = {
|
||||
selectedProjectId: PropTypes.string,
|
||||
selectedProjectEntity: PropTypes.object,
|
||||
setSelectedProjectEntity: PropTypes.func.isRequired,
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Uppy from '@uppy/core'
|
||||
import XHRUpload from '@uppy/xhr-upload'
|
||||
import { Dashboard } from '@uppy/react'
|
||||
|
@ -18,23 +17,23 @@ import { debugConsole } from '@/utils/debugging'
|
|||
export default function FileTreeUploadDoc() {
|
||||
const { parentFolderId, cancel, isDuplicate, droppedFiles, setDroppedFiles } =
|
||||
useFileTreeActionable()
|
||||
const { _id: projectId } = useProjectContext(projectContextPropTypes)
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
const [error, setError] = useState()
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
const [conflicts, setConflicts] = useState([])
|
||||
const [conflicts, setConflicts] = useState<any[]>([])
|
||||
const [overwrite, setOverwrite] = useState(false)
|
||||
|
||||
const maxNumberOfFiles = 40
|
||||
const maxFileSize = window.ExposedSettings.maxUploadSize
|
||||
|
||||
// calculate conflicts
|
||||
const buildConflicts = files =>
|
||||
const buildConflicts = (files: Record<string, any>) =>
|
||||
Object.values(files).filter(file =>
|
||||
isDuplicate(file.meta.targetFolderId ?? parentFolderId, file.meta.name)
|
||||
)
|
||||
|
||||
const buildEndpoint = (projectId, targetFolderId) => {
|
||||
const buildEndpoint = (projectId: string, targetFolderId: string) => {
|
||||
let endpoint = `/project/${projectId}/upload`
|
||||
|
||||
if (targetFolderId) {
|
||||
|
@ -142,6 +141,7 @@ export default function FileTreeUploadDoc() {
|
|||
const uppyFile = uppy.getFile(fileId)
|
||||
uppy.setFileState(fileId, {
|
||||
xhrUpload: {
|
||||
// @ts-ignore
|
||||
...uppyFile.xhrUpload,
|
||||
endpoint: buildEndpoint(projectId, droppedFiles.targetFolderId),
|
||||
},
|
||||
|
@ -204,28 +204,39 @@ export default function FileTreeUploadDoc() {
|
|||
)
|
||||
}
|
||||
|
||||
const projectContextPropTypes = {
|
||||
_id: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
function UploadErrorMessage({ error, maxNumberOfFiles }) {
|
||||
function UploadErrorMessage({
|
||||
error,
|
||||
maxNumberOfFiles,
|
||||
}: {
|
||||
error: string
|
||||
maxNumberOfFiles: number
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
switch (error) {
|
||||
case 'too-many-files':
|
||||
return t('maximum_files_uploaded_together', {
|
||||
max: maxNumberOfFiles,
|
||||
})
|
||||
return (
|
||||
<>
|
||||
{t('maximum_files_uploaded_together', {
|
||||
max: maxNumberOfFiles,
|
||||
})}
|
||||
</>
|
||||
)
|
||||
|
||||
default:
|
||||
return <ErrorMessage error={error} />
|
||||
}
|
||||
}
|
||||
UploadErrorMessage.propTypes = {
|
||||
error: PropTypes.string.isRequired,
|
||||
maxNumberOfFiles: PropTypes.number.isRequired,
|
||||
}
|
||||
|
||||
function UploadConflicts({ cancel, conflicts, handleOverwrite }) {
|
||||
function UploadConflicts({
|
||||
cancel,
|
||||
conflicts,
|
||||
handleOverwrite,
|
||||
}: {
|
||||
cancel: () => void
|
||||
conflicts: any[]
|
||||
handleOverwrite: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
|
@ -258,8 +269,3 @@ function UploadConflicts({ cancel, conflicts, handleOverwrite }) {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
UploadConflicts.propTypes = {
|
||||
cancel: PropTypes.func.isRequired,
|
||||
conflicts: PropTypes.array.isRequired,
|
||||
handleOverwrite: PropTypes.func.isRequired,
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useProjectContext } from '../../../../shared/context/project-context'
|
||||
import { useLocation } from '../../../../shared/hooks/use-location'
|
||||
|
@ -7,7 +6,7 @@ import { useLocation } from '../../../../shared/hooks/use-location'
|
|||
// handle "not-logged-in" errors by redirecting to the login page
|
||||
export default function RedirectToLogin() {
|
||||
const { t } = useTranslation()
|
||||
const { _id: projectId } = useProjectContext(projectContextPropTypes)
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const [secondsToRedirect, setSecondsToRedirect] = useState(10)
|
||||
const location = useLocation()
|
||||
|
||||
|
@ -35,7 +34,3 @@ export default function RedirectToLogin() {
|
|||
seconds: secondsToRedirect,
|
||||
})
|
||||
}
|
||||
|
||||
const projectContextPropTypes = {
|
||||
_id: PropTypes.string.isRequired,
|
||||
}
|
|
@ -1,11 +1,7 @@
|
|||
import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
|
||||
import { useCallback } from 'react'
|
||||
import { FC, useCallback } from 'react'
|
||||
|
||||
type FileTreeInnerProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function FileTreeInner({ children }: FileTreeInnerProps) {
|
||||
const FileTreeInner: FC = ({ children }) => {
|
||||
const { setIsRootFolderSelected, selectedEntityIds, select } =
|
||||
useFileTreeSelectable()
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ReactNode, useEffect, useRef } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed'
|
||||
|
||||
|
@ -13,8 +12,18 @@ import { useFileTreeSelectable } from '../../contexts/file-tree-selectable'
|
|||
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
|
||||
import { useDragDropManager } from 'react-dnd'
|
||||
|
||||
function FileTreeItemInner({ id, name, isSelected, icons }) {
|
||||
const { permissionsLevel } = useEditorContext(editorContextPropTypes)
|
||||
function FileTreeItemInner({
|
||||
id,
|
||||
name,
|
||||
isSelected,
|
||||
icons,
|
||||
}: {
|
||||
id: string
|
||||
name: string
|
||||
isSelected: boolean
|
||||
icons?: ReactNode
|
||||
}) {
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
const { setContextMenuCoords } = useFileTreeMainContext()
|
||||
const { isRenaming } = useFileTreeActionable()
|
||||
|
||||
|
@ -29,7 +38,7 @@ function FileTreeItemInner({ id, name, isSelected, icons }) {
|
|||
|
||||
const dragDropItem = useDragDropManager().getMonitor().getItem()
|
||||
|
||||
const itemRef = useRef()
|
||||
const itemRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const item = itemRef.current
|
||||
|
@ -49,7 +58,7 @@ function FileTreeItemInner({ id, name, isSelected, icons }) {
|
|||
}
|
||||
}, [isSelected, itemRef])
|
||||
|
||||
function handleContextMenu(ev) {
|
||||
function handleContextMenu(ev: React.MouseEvent<HTMLDivElement>) {
|
||||
ev.preventDefault()
|
||||
|
||||
setContextMenuCoords({
|
||||
|
@ -85,15 +94,4 @@ function FileTreeItemInner({ id, name, isSelected, icons }) {
|
|||
)
|
||||
}
|
||||
|
||||
FileTreeItemInner.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
icons: PropTypes.node,
|
||||
}
|
||||
|
||||
const editorContextPropTypes = {
|
||||
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
|
||||
}
|
||||
|
||||
export default FileTreeItemInner
|
|
@ -1,6 +1,4 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
|
||||
|
@ -13,16 +11,23 @@ import FileTreeModalCreateFolder from './modals/file-tree-modal-create-folder'
|
|||
import FileTreeModalError from './modals/file-tree-modal-error'
|
||||
import FileTreeContextMenu from './file-tree-context-menu'
|
||||
import FileTreeError from './file-tree-error'
|
||||
|
||||
import { useDroppable } from '../contexts/file-tree-draggable'
|
||||
|
||||
import { useFileTreeSocketListener } from '../hooks/file-tree-socket-listener'
|
||||
import FileTreeModalCreateFile from './modals/file-tree-modal-create-file'
|
||||
import FileTreeInner from './file-tree-inner'
|
||||
import { useDragLayer } from 'react-dnd'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const FileTreeRoot = React.memo(function FileTreeRoot({
|
||||
const FileTreeRoot = React.memo<{
|
||||
onSelect: () => void
|
||||
onDelete: () => void
|
||||
onInit: () => void
|
||||
isConnected: boolean
|
||||
setRefProviderEnabled: () => void
|
||||
setStartedFreeTrial: () => void
|
||||
reindexReferences: () => void
|
||||
refProviders: Record<string, boolean>
|
||||
}>(function FileTreeRoot({
|
||||
refProviders,
|
||||
reindexReferences,
|
||||
setRefProviderEnabled,
|
||||
|
@ -32,7 +37,7 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
|
|||
onDelete,
|
||||
isConnected,
|
||||
}) {
|
||||
const { _id: projectId } = useProjectContext(projectContextPropTypes)
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
const isReady = Boolean(projectId && fileTreeData)
|
||||
|
||||
|
@ -63,7 +68,7 @@ const FileTreeRoot = React.memo(function FileTreeRoot({
|
|||
)
|
||||
})
|
||||
|
||||
function FileTreeRootFolder({ onDelete }) {
|
||||
function FileTreeRootFolder({ onDelete }: { onDelete: () => void }) {
|
||||
useFileTreeSocketListener(onDelete)
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
|
||||
|
@ -87,30 +92,11 @@ function FileTreeRootFolder({ onDelete }) {
|
|||
'file-tree-dragging': dragLayer.isDragging,
|
||||
}),
|
||||
}}
|
||||
dropRef={dropRef}
|
||||
dropRef={dropRef as any}
|
||||
dataTestId="file-tree-list-root"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
FileTreeRootFolder.propTypes = {
|
||||
onDelete: PropTypes.func,
|
||||
}
|
||||
|
||||
FileTreeRoot.propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onInit: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func,
|
||||
isConnected: PropTypes.bool.isRequired,
|
||||
setRefProviderEnabled: PropTypes.func.isRequired,
|
||||
setStartedFreeTrial: PropTypes.func.isRequired,
|
||||
reindexReferences: PropTypes.func.isRequired,
|
||||
refProviders: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
const projectContextPropTypes = {
|
||||
_id: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default withErrorBoundary(FileTreeRoot, FileTreeError)
|
|
@ -1,4 +1,3 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
|
||||
|
@ -10,7 +9,7 @@ import { useEditorContext } from '../../../shared/context/editor-context'
|
|||
import { useFileTreeActionable } from '../contexts/file-tree-actionable'
|
||||
|
||||
function FileTreeToolbar() {
|
||||
const { permissionsLevel } = useEditorContext(editorContextPropTypes)
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
|
||||
if (permissionsLevel === 'readOnly') return null
|
||||
|
||||
|
@ -22,10 +21,6 @@ function FileTreeToolbar() {
|
|||
)
|
||||
}
|
||||
|
||||
const editorContextPropTypes = {
|
||||
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
|
||||
}
|
||||
|
||||
function FileTreeToolbarLeft() {
|
||||
const { t } = useTranslation()
|
||||
const {
|
|
@ -6,8 +6,8 @@ import {
|
|||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
FC,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { mapSeries } from '../../../infrastructure/promise'
|
||||
|
||||
|
@ -32,24 +32,73 @@ import {
|
|||
DuplicateFilenameError,
|
||||
DuplicateFilenameMoveError,
|
||||
} from '../errors'
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
|
||||
const FileTreeActionableContext = createContext()
|
||||
const FileTreeActionableContext = createContext<
|
||||
| {
|
||||
isDeleting: boolean
|
||||
isRenaming: boolean
|
||||
isCreatingFile: boolean
|
||||
isCreatingFolder: boolean
|
||||
isMoving: boolean
|
||||
inFlight: boolean
|
||||
actionedEntities: any | null
|
||||
newFileCreateMode: any | null
|
||||
error: any | null
|
||||
canDelete: boolean
|
||||
canRename: boolean
|
||||
canCreate: boolean
|
||||
parentFolderId: string
|
||||
isDuplicate: (parentFolderId: string, name: string) => boolean
|
||||
startRenaming: any
|
||||
finishRenaming: any
|
||||
startDeleting: any
|
||||
finishDeleting: any
|
||||
finishMoving: any
|
||||
startCreatingFile: any
|
||||
startCreatingFolder: any
|
||||
finishCreatingFolder: any
|
||||
startCreatingDocOrFile: any
|
||||
startUploadingDocOrFile: any
|
||||
finishCreatingDoc: any
|
||||
finishCreatingLinkedFile: any
|
||||
cancel: () => void
|
||||
droppedFiles: { files: any; targetFolderId: string } | null
|
||||
setDroppedFiles: (files: any) => void
|
||||
downloadPath?: string
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
const ACTION_TYPES = {
|
||||
START_RENAME: 'START_RENAME',
|
||||
START_DELETE: 'START_DELETE',
|
||||
DELETING: 'DELETING',
|
||||
START_CREATE_FILE: 'START_CREATE_FILE',
|
||||
START_CREATE_FOLDER: 'START_CREATE_FOLDER',
|
||||
CREATING_FILE: 'CREATING_FILE',
|
||||
CREATING_FOLDER: 'CREATING_FOLDER',
|
||||
MOVING: 'MOVING',
|
||||
CANCEL: 'CANCEL',
|
||||
CLEAR: 'CLEAR',
|
||||
ERROR: 'ERROR',
|
||||
/* eslint-disable no-unused-vars */
|
||||
enum ACTION_TYPES {
|
||||
START_RENAME = 'START_RENAME',
|
||||
START_DELETE = 'START_DELETE',
|
||||
DELETING = 'DELETING',
|
||||
START_CREATE_FILE = 'START_CREATE_FILE',
|
||||
START_CREATE_FOLDER = 'START_CREATE_FOLDER',
|
||||
CREATING_FILE = 'CREATING_FILE',
|
||||
CREATING_FOLDER = 'CREATING_FOLDER',
|
||||
MOVING = 'MOVING',
|
||||
CANCEL = 'CANCEL',
|
||||
CLEAR = 'CLEAR',
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
type State = {
|
||||
isDeleting: boolean
|
||||
isRenaming: boolean
|
||||
isCreatingFile: boolean
|
||||
isCreatingFolder: boolean
|
||||
isMoving: boolean
|
||||
inFlight: boolean
|
||||
actionedEntities: any | null
|
||||
newFileCreateMode: any | null
|
||||
error: unknown | null
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
const defaultState: State = {
|
||||
isDeleting: false,
|
||||
isRenaming: false,
|
||||
isCreatingFile: false,
|
||||
|
@ -61,11 +110,49 @@ const defaultState = {
|
|||
error: null,
|
||||
}
|
||||
|
||||
function fileTreeActionableReadOnlyReducer(state) {
|
||||
function fileTreeActionableReadOnlyReducer(state: State) {
|
||||
return state
|
||||
}
|
||||
|
||||
function fileTreeActionableReducer(state, action) {
|
||||
type Action =
|
||||
| {
|
||||
type: ACTION_TYPES.START_RENAME
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.START_DELETE
|
||||
actionedEntities: any | null
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.START_CREATE_FILE
|
||||
newFileCreateMode: any | null
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.START_CREATE_FOLDER
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.CREATING_FILE
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.CREATING_FOLDER
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.DELETING
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.MOVING
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.CLEAR
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.CANCEL
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.ERROR
|
||||
error: unknown
|
||||
}
|
||||
|
||||
function fileTreeActionableReducer(state: State, action: Action) {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPES.START_RENAME:
|
||||
return { ...defaultState, isRenaming: true }
|
||||
|
@ -115,13 +202,15 @@ function fileTreeActionableReducer(state, action) {
|
|||
case ACTION_TYPES.ERROR:
|
||||
return { ...state, inFlight: false, error: action.error }
|
||||
default:
|
||||
throw new Error(`Unknown user action type: ${action.type}`)
|
||||
throw new Error(`Unknown user action type: ${(action as Action).type}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function FileTreeActionableProvider({ reindexReferences, children }) {
|
||||
const { _id: projectId } = useProjectContext(projectContextPropTypes)
|
||||
const { permissionsLevel } = useEditorContext(editorContextPropTypes)
|
||||
export const FileTreeActionableProvider: FC<{
|
||||
reindexReferences: () => void
|
||||
}> = ({ reindexReferences, children }) => {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
|
||||
const [state, dispatch] = useReducer(
|
||||
permissionsLevel === 'readOnly'
|
||||
|
@ -142,7 +231,7 @@ export function FileTreeActionableProvider({ reindexReferences, children }) {
|
|||
// update the entity with the new name immediately in the tree, but revert to
|
||||
// the old name if the sync fails
|
||||
const finishRenaming = useCallback(
|
||||
newName => {
|
||||
(newName: string) => {
|
||||
const selectedEntityId = Array.from(selectedEntityIds)[0]
|
||||
const found = findInTreeOrThrow(fileTreeData, selectedEntityId)
|
||||
const oldName = found.entity.name
|
||||
|
@ -168,7 +257,7 @@ export function FileTreeActionableProvider({ reindexReferences, children }) {
|
|||
)
|
||||
|
||||
const isDuplicate = useCallback(
|
||||
(parentFolderId, name) => {
|
||||
(parentFolderId: string, name: string) => {
|
||||
return !isNameUniqueInFolder(fileTreeData, parentFolderId, name)
|
||||
},
|
||||
[fileTreeData]
|
||||
|
@ -189,29 +278,32 @@ export function FileTreeActionableProvider({ reindexReferences, children }) {
|
|||
dispatch({ type: ACTION_TYPES.DELETING })
|
||||
let shouldReindexReferences = false
|
||||
|
||||
return mapSeries(Array.from(selectedEntityIds), id => {
|
||||
const found = findInTreeOrThrow(fileTreeData, id)
|
||||
shouldReindexReferences =
|
||||
shouldReindexReferences || /\.bib$/.test(found.entity.name)
|
||||
return syncDelete(projectId, found.type, found.entity._id).catch(
|
||||
error => {
|
||||
// throw unless 404
|
||||
if (error.info.statusCode !== 404) {
|
||||
throw error
|
||||
return (
|
||||
mapSeries(Array.from(selectedEntityIds), id => {
|
||||
const found = findInTreeOrThrow(fileTreeData, id)
|
||||
shouldReindexReferences =
|
||||
shouldReindexReferences || /\.bib$/.test(found.entity.name)
|
||||
return syncDelete(projectId, found.type, found.entity._id).catch(
|
||||
error => {
|
||||
// throw unless 404
|
||||
if (error.info.statusCode !== 404) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
if (shouldReindexReferences) {
|
||||
reindexReferences()
|
||||
}
|
||||
dispatch({ type: ACTION_TYPES.CLEAR })
|
||||
})
|
||||
.catch(error => {
|
||||
// set an error and allow user to retry
|
||||
dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
)
|
||||
})
|
||||
// @ts-ignore (TODO: improve mapSeries types)
|
||||
.then(() => {
|
||||
if (shouldReindexReferences) {
|
||||
reindexReferences()
|
||||
}
|
||||
dispatch({ type: ACTION_TYPES.CLEAR })
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
// set an error and allow user to retry
|
||||
dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
})
|
||||
)
|
||||
}, [fileTreeData, projectId, selectedEntityIds, reindexReferences])
|
||||
|
||||
// moves entities. Tree is updated immediately and data are sync'd after.
|
||||
|
@ -242,7 +334,7 @@ export function FileTreeActionableProvider({ reindexReferences, children }) {
|
|||
}
|
||||
|
||||
// keep track of old parent folder ids so we can revert entities if sync fails
|
||||
const oldParentFolderIds = {}
|
||||
const oldParentFolderIds: Record<string, string> = {}
|
||||
let isMoveFailed = false
|
||||
|
||||
// dispatch moves immediately
|
||||
|
@ -252,19 +344,23 @@ export function FileTreeActionableProvider({ reindexReferences, children }) {
|
|||
})
|
||||
|
||||
// sync dispatched moves after
|
||||
return mapSeries(founds, async found => {
|
||||
try {
|
||||
await syncMove(projectId, found.type, found.entity._id, toFolderId)
|
||||
} catch (error) {
|
||||
isMoveFailed = true
|
||||
dispatchMove(found.entity._id, oldParentFolderIds[found.entity._id])
|
||||
dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
}
|
||||
}).then(() => {
|
||||
if (!isMoveFailed) {
|
||||
dispatch({ type: ACTION_TYPES.CLEAR })
|
||||
}
|
||||
})
|
||||
return (
|
||||
mapSeries(founds, async found => {
|
||||
try {
|
||||
await syncMove(projectId, found.type, found.entity._id, toFolderId)
|
||||
} catch (error) {
|
||||
isMoveFailed = true
|
||||
dispatchMove(found.entity._id, oldParentFolderIds[found.entity._id])
|
||||
dispatch({ type: ACTION_TYPES.ERROR, error })
|
||||
}
|
||||
})
|
||||
// @ts-ignore (TODO: improve mapSeries types)
|
||||
.then(() => {
|
||||
if (!isMoveFailed) {
|
||||
dispatch({ type: ACTION_TYPES.CLEAR })
|
||||
}
|
||||
})
|
||||
)
|
||||
},
|
||||
[dispatchMove, fileTreeData, projectId]
|
||||
)
|
||||
|
@ -356,10 +452,10 @@ export function FileTreeActionableProvider({ reindexReferences, children }) {
|
|||
|
||||
// listen for `file-tree.start-creating` events
|
||||
useEffect(() => {
|
||||
function handleEvent(event) {
|
||||
function handleEvent(event: Event) {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.START_CREATE_FILE,
|
||||
newFileCreateMode: event.detail.mode,
|
||||
newFileCreateMode: (event as CustomEvent<{ mode: string }>).detail.mode,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -381,6 +477,7 @@ export function FileTreeActionableProvider({ reindexReferences, children }) {
|
|||
}
|
||||
}, [fileTreeData, projectId, selectedEntityIds])
|
||||
|
||||
// TODO: wrap in useMemo
|
||||
const value = {
|
||||
canDelete: selectedEntityIds.size > 0 && !isRootFolderSelected,
|
||||
canRename: selectedEntityIds.size === 1 && !isRootFolderSelected,
|
||||
|
@ -413,22 +510,6 @@ export function FileTreeActionableProvider({ reindexReferences, children }) {
|
|||
)
|
||||
}
|
||||
|
||||
FileTreeActionableProvider.propTypes = {
|
||||
reindexReferences: PropTypes.func.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
}
|
||||
|
||||
const projectContextPropTypes = {
|
||||
_id: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
const editorContextPropTypes = {
|
||||
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
|
||||
}
|
||||
|
||||
export function useFileTreeActionable() {
|
||||
const context = useContext(FileTreeActionableContext)
|
||||
|
||||
|
@ -442,9 +523,9 @@ export function useFileTreeActionable() {
|
|||
}
|
||||
|
||||
function getSelectedParentFolderId(
|
||||
fileTreeData,
|
||||
selectedEntityIds,
|
||||
isRootFolderSelected
|
||||
fileTreeData: Folder,
|
||||
selectedEntityIds: Set<string>,
|
||||
isRootFolderSelected: boolean
|
||||
) {
|
||||
if (isRootFolderSelected) {
|
||||
return fileTreeData._id
|
||||
|
@ -467,7 +548,11 @@ function getSelectedParentFolderId(
|
|||
return found.type === 'folder' ? found.entity._id : found.parentFolderId
|
||||
}
|
||||
|
||||
function validateCreate(fileTreeData, parentFolderId, entity) {
|
||||
function validateCreate(
|
||||
fileTreeData: Folder,
|
||||
parentFolderId: string,
|
||||
entity: { name: string; endpoint: string }
|
||||
) {
|
||||
if (!isCleanFilename(entity.name)) {
|
||||
return new InvalidFilenameError()
|
||||
}
|
||||
|
@ -484,7 +569,11 @@ function validateCreate(fileTreeData, parentFolderId, entity) {
|
|||
}
|
||||
}
|
||||
|
||||
function validateRename(fileTreeData, found, newName) {
|
||||
function validateRename(
|
||||
fileTreeData: Folder,
|
||||
found: { parentFolderId: string; path: string; type: string },
|
||||
newName: string
|
||||
) {
|
||||
if (!isCleanFilename(newName)) {
|
||||
return new InvalidFilenameError()
|
||||
}
|
||||
|
@ -500,10 +589,16 @@ function validateRename(fileTreeData, found, newName) {
|
|||
}
|
||||
}
|
||||
|
||||
function validateMove(fileTreeData, toFolderId, found, isMoveToRoot) {
|
||||
function validateMove(
|
||||
fileTreeData: Folder,
|
||||
toFolderId: string,
|
||||
found: { entity: { name: string }; type: string },
|
||||
isMoveToRoot: boolean
|
||||
) {
|
||||
if (!isNameUniqueInFolder(fileTreeData, toFolderId, found.entity.name)) {
|
||||
const error = new DuplicateFilenameMoveError()
|
||||
error.entityName = found.entity.name
|
||||
;(error as DuplicateFilenameMoveError & { entityName: string }).entityName =
|
||||
found.entity.name
|
||||
return error
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { createContext, useContext, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { createContext, FC, useContext, useState } from 'react'
|
||||
|
||||
const FileTreeCreateFormContext = createContext()
|
||||
const FileTreeCreateFormContext = createContext<
|
||||
{ valid: boolean; setValid: (value: boolean) => void } | undefined
|
||||
>(undefined)
|
||||
|
||||
export const useFileTreeCreateForm = () => {
|
||||
const context = useContext(FileTreeCreateFormContext)
|
||||
|
@ -15,7 +16,7 @@ export const useFileTreeCreateForm = () => {
|
|||
return context
|
||||
}
|
||||
|
||||
export default function FileTreeCreateFormProvider({ children }) {
|
||||
const FileTreeCreateFormProvider: FC = ({ children }) => {
|
||||
// is the form valid
|
||||
const [valid, setValid] = useState(false)
|
||||
|
||||
|
@ -26,9 +27,4 @@ export default function FileTreeCreateFormProvider({ children }) {
|
|||
)
|
||||
}
|
||||
|
||||
FileTreeCreateFormProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
}
|
||||
export default FileTreeCreateFormProvider
|
|
@ -1,8 +1,15 @@
|
|||
import { createContext, useContext, useMemo, useReducer } from 'react'
|
||||
import { createContext, FC, useContext, useMemo, useReducer } from 'react'
|
||||
import { isCleanFilename } from '../util/safe-path'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const FileTreeCreateNameContext = createContext()
|
||||
const FileTreeCreateNameContext = createContext<
|
||||
| {
|
||||
name: string
|
||||
touchedName: boolean
|
||||
validName: boolean
|
||||
setName: (name: string) => void
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
export const useFileTreeCreateName = () => {
|
||||
const context = useContext(FileTreeCreateNameContext)
|
||||
|
@ -16,12 +23,17 @@ export const useFileTreeCreateName = () => {
|
|||
return context
|
||||
}
|
||||
|
||||
export default function FileTreeCreateNameProvider({
|
||||
type State = {
|
||||
name: string
|
||||
touchedName: boolean
|
||||
}
|
||||
|
||||
const FileTreeCreateNameProvider: FC<{ initialName?: string }> = ({
|
||||
children,
|
||||
initialName = '',
|
||||
}) {
|
||||
}) => {
|
||||
const [state, setName] = useReducer(
|
||||
(state, name) => ({
|
||||
(state: State, name: string) => ({
|
||||
name, // the file name
|
||||
touchedName: true, // whether the name has been edited
|
||||
}),
|
||||
|
@ -43,10 +55,4 @@ export default function FileTreeCreateNameProvider({
|
|||
)
|
||||
}
|
||||
|
||||
FileTreeCreateNameProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
initialName: PropTypes.string,
|
||||
}
|
||||
export default FileTreeCreateNameProvider
|
|
@ -1,5 +1,4 @@
|
|||
import { useRef, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useRef, useEffect, useState, FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles'
|
||||
import { DndProvider, createDndContext, useDrag, useDrop } from 'react-dnd'
|
||||
|
@ -25,12 +24,13 @@ import { useEditorContext } from '../../../shared/context/editor-context'
|
|||
// particular in rich text.
|
||||
// This is a hacky workaround to avoid calling the DnD listeners when the
|
||||
// draggable or droppable element is not within a `dnd-container` element.
|
||||
const ModifiedBackend = (...args) => {
|
||||
function isDndChild(elt) {
|
||||
const ModifiedBackend = (...args: any[]) => {
|
||||
function isDndChild(elt: Element): boolean {
|
||||
if (elt.getAttribute && elt.getAttribute('dnd-container')) return true
|
||||
if (!elt.parentNode) return false
|
||||
return isDndChild(elt.parentNode)
|
||||
return isDndChild(elt.parentNode as Element)
|
||||
}
|
||||
// @ts-ignore
|
||||
const instance = new HTML5Backend(...args)
|
||||
|
||||
const dragDropListeners = [
|
||||
|
@ -48,8 +48,8 @@ const ModifiedBackend = (...args) => {
|
|||
|
||||
dragDropListeners.forEach(dragDropListener => {
|
||||
const originalListener = instance[dragDropListener]
|
||||
instance[dragDropListener] = (ev, ...extraArgs) => {
|
||||
if (isDndChild(ev.target)) originalListener(ev, ...extraArgs)
|
||||
instance[dragDropListener] = (ev: Event, ...extraArgs: any[]) => {
|
||||
if (isDndChild(ev.target as Element)) originalListener(ev, ...extraArgs)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -59,28 +59,20 @@ const ModifiedBackend = (...args) => {
|
|||
const DndContext = createDndContext(ModifiedBackend)
|
||||
|
||||
const DRAGGABLE_TYPE = 'ENTITY'
|
||||
|
||||
export function FileTreeDraggableProvider({ children }) {
|
||||
export const FileTreeDraggableProvider: FC = ({ children }) => {
|
||||
const DndManager = useRef(DndContext)
|
||||
|
||||
return (
|
||||
<DndProvider manager={DndManager.current.dragDropManager}>
|
||||
<DndProvider manager={DndManager.current.dragDropManager!}>
|
||||
{children}
|
||||
</DndProvider>
|
||||
)
|
||||
}
|
||||
|
||||
FileTreeDraggableProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
}
|
||||
|
||||
export function useDraggable(draggedEntityId) {
|
||||
export function useDraggable(draggedEntityId: string) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { permissionsLevel } = useEditorContext(editorContextPropTypes)
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
const { selectedEntityIds, isRootFolderSelected } = useFileTreeSelectable()
|
||||
|
||||
|
@ -101,6 +93,7 @@ export function useDraggable(draggedEntityId) {
|
|||
},
|
||||
collect: monitor => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
draggedEntityIds: monitor.getItem()?.draggedEntityIds,
|
||||
}),
|
||||
canDrag: () => permissionsLevel !== 'readOnly' && isDraggable,
|
||||
end: () => item,
|
||||
|
@ -120,15 +113,11 @@ export function useDraggable(draggedEntityId) {
|
|||
}
|
||||
}
|
||||
|
||||
const editorContextPropTypes = {
|
||||
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
|
||||
}
|
||||
|
||||
export function useDroppable(droppedEntityId) {
|
||||
export function useDroppable(droppedEntityId: string) {
|
||||
const { finishMoving, setDroppedFiles, startUploadingDocOrFile } =
|
||||
useFileTreeActionable()
|
||||
|
||||
const [{ isOver }, dropRef] = useDrop({
|
||||
const [{ isOver }, dropRef] = useDrop<any, any, any>({
|
||||
accept: [DRAGGABLE_TYPE, NativeTypes.FILE],
|
||||
canDrop: (item, monitor) => {
|
||||
const isOver = monitor.isOver({ shallow: true })
|
||||
|
@ -166,7 +155,10 @@ export function useDroppable(droppedEntityId) {
|
|||
// Get the list of dragged entity ids. If the dragged entity is one of the
|
||||
// selected entities then all the selected entites are dragged entities,
|
||||
// otherwise it's the dragged entity only.
|
||||
function getDraggedEntityIds(selectedEntityIds, draggedEntityId) {
|
||||
function getDraggedEntityIds(
|
||||
selectedEntityIds: Set<string>,
|
||||
draggedEntityId: string
|
||||
) {
|
||||
if (selectedEntityIds.size > 1 && selectedEntityIds.has(draggedEntityId)) {
|
||||
// dragging the multi-selected entities
|
||||
return new Set(selectedEntityIds)
|
||||
|
@ -178,7 +170,10 @@ function getDraggedEntityIds(selectedEntityIds, draggedEntityId) {
|
|||
|
||||
// Get the draggable title. This is the name of the dragged entities if there's
|
||||
// only one, otherwise it's the number of dragged entities.
|
||||
function getDraggedTitle(draggedItems, t) {
|
||||
function getDraggedTitle(
|
||||
draggedItems: Set<any>,
|
||||
t: (key: string, options: Record<string, any>) => void
|
||||
) {
|
||||
if (draggedItems.size === 1) {
|
||||
const draggedItem = Array.from(draggedItems)[0]
|
||||
return draggedItem.entity.name
|
||||
|
@ -187,7 +182,7 @@ function getDraggedTitle(draggedItems, t) {
|
|||
}
|
||||
|
||||
// Get all children folder ids of any of the dragged items.
|
||||
function getForbiddenFolderIds(draggedItems) {
|
||||
function getForbiddenFolderIds(draggedItems: Set<any>) {
|
||||
const draggedFoldersArray = Array.from(draggedItems)
|
||||
.filter(draggedItem => {
|
||||
return draggedItem.type === 'folder'
|
|
@ -1,52 +0,0 @@
|
|||
import { createContext, useContext, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const FileTreeMainContext = createContext()
|
||||
|
||||
export function useFileTreeMainContext() {
|
||||
const context = useContext(FileTreeMainContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useFileTreeMainContext is only available inside FileTreeMainProvider'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export const FileTreeMainProvider = function ({
|
||||
refProviders,
|
||||
reindexReferences,
|
||||
setRefProviderEnabled,
|
||||
setStartedFreeTrial,
|
||||
children,
|
||||
}) {
|
||||
const [contextMenuCoords, setContextMenuCoords] = useState()
|
||||
|
||||
return (
|
||||
<FileTreeMainContext.Provider
|
||||
value={{
|
||||
refProviders,
|
||||
reindexReferences,
|
||||
setRefProviderEnabled,
|
||||
setStartedFreeTrial,
|
||||
contextMenuCoords,
|
||||
setContextMenuCoords,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FileTreeMainContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
FileTreeMainProvider.propTypes = {
|
||||
reindexReferences: PropTypes.func.isRequired,
|
||||
refProviders: PropTypes.object.isRequired,
|
||||
setRefProviderEnabled: PropTypes.func.isRequired,
|
||||
setStartedFreeTrial: PropTypes.func.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import { createContext, FC, useContext, useState } from 'react'
|
||||
|
||||
type ContextMenuCoords = { top: number; left: number }
|
||||
|
||||
const FileTreeMainContext = createContext<
|
||||
| {
|
||||
refProviders: object
|
||||
reindexReferences: () => void
|
||||
setRefProviderEnabled: (provider: string, value: boolean) => void
|
||||
setStartedFreeTrial: (value: boolean) => void
|
||||
contextMenuCoords: ContextMenuCoords | null
|
||||
setContextMenuCoords: (value: ContextMenuCoords | null) => void
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
export function useFileTreeMainContext() {
|
||||
const context = useContext(FileTreeMainContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useFileTreeMainContext is only available inside FileTreeMainProvider'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export const FileTreeMainProvider: FC<{
|
||||
reindexReferences: () => void
|
||||
refProviders: object
|
||||
setRefProviderEnabled: (provider: string, value: boolean) => void
|
||||
setStartedFreeTrial: (value: boolean) => void
|
||||
}> = ({
|
||||
refProviders,
|
||||
reindexReferences,
|
||||
setRefProviderEnabled,
|
||||
setStartedFreeTrial,
|
||||
children,
|
||||
}) => {
|
||||
const [contextMenuCoords, setContextMenuCoords] =
|
||||
useState<ContextMenuCoords | null>(null)
|
||||
|
||||
return (
|
||||
<FileTreeMainContext.Provider
|
||||
value={{
|
||||
refProviders,
|
||||
reindexReferences,
|
||||
setRefProviderEnabled,
|
||||
setStartedFreeTrial,
|
||||
contextMenuCoords,
|
||||
setContextMenuCoords,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FileTreeMainContext.Provider>
|
||||
)
|
||||
}
|
|
@ -6,11 +6,10 @@ import {
|
|||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
FC,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classNames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
import { findInTree } from '../util/find-in-tree'
|
||||
import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
|
@ -19,17 +18,53 @@ import { useLayoutContext } from '../../../shared/context/layout-context'
|
|||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||
import usePreviousValue from '../../../shared/hooks/use-previous-value'
|
||||
import { useFileTreeMainContext } from '@/features/file-tree/contexts/file-tree-main'
|
||||
import { FindResult } from '@/features/file-tree/util/path'
|
||||
import { fileCollator } from '@/features/file-tree/util/file-collator'
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
import { FileTreeEntity } from '../../../../../types/file-tree-entity'
|
||||
|
||||
const FileTreeSelectableContext = createContext()
|
||||
const FileTreeSelectableContext = createContext<
|
||||
| {
|
||||
selectedEntityIds: Set<string>
|
||||
isRootFolderSelected: boolean
|
||||
selectOrMultiSelectEntity: (
|
||||
id: string | string[],
|
||||
multiple?: boolean
|
||||
) => void
|
||||
setIsRootFolderSelected: (value: boolean) => void
|
||||
selectedEntityParentIds: Set<string>
|
||||
select: (id: string | string[]) => void
|
||||
unselect: (id: string) => void
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
const ACTION_TYPES = {
|
||||
SELECT: 'SELECT',
|
||||
MULTI_SELECT: 'MULTI_SELECT',
|
||||
UNSELECT: 'UNSELECT',
|
||||
/* eslint-disable no-unused-vars */
|
||||
enum ACTION_TYPES {
|
||||
SELECT = 'SELECT',
|
||||
MULTI_SELECT = 'MULTI_SELECT',
|
||||
UNSELECT = 'UNSELECT',
|
||||
}
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
function fileTreeSelectableReadWriteReducer(selectedEntityIds, action) {
|
||||
type Action =
|
||||
| {
|
||||
type: ACTION_TYPES.SELECT
|
||||
id: string
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.MULTI_SELECT
|
||||
id: string
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.UNSELECT
|
||||
id: string
|
||||
}
|
||||
|
||||
function fileTreeSelectableReadWriteReducer(
|
||||
selectedEntityIds: Set<string>,
|
||||
action: Action
|
||||
) {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPES.SELECT: {
|
||||
// reset selection
|
||||
|
@ -59,11 +94,16 @@ function fileTreeSelectableReadWriteReducer(selectedEntityIds, action) {
|
|||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown selectable action type: ${action.type}`)
|
||||
throw new Error(
|
||||
`Unknown selectable action type: ${(action as Action).type}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function fileTreeSelectableReadOnlyReducer(selectedEntityIds, action) {
|
||||
function fileTreeSelectableReadOnlyReducer(
|
||||
selectedEntityIds: Set<string>,
|
||||
action: Action
|
||||
) {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPES.SELECT:
|
||||
return new Set([action.id])
|
||||
|
@ -73,15 +113,17 @@ function fileTreeSelectableReadOnlyReducer(selectedEntityIds, action) {
|
|||
return selectedEntityIds
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown selectable action type: ${action.type}`)
|
||||
throw new Error(
|
||||
`Unknown selectable action type: ${(action as Action).type}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function FileTreeSelectableProvider({ onSelect, children }) {
|
||||
const { _id: projectId, rootDocId } = useProjectContext(
|
||||
projectContextPropTypes
|
||||
)
|
||||
const { permissionsLevel } = useEditorContext(editorContextPropTypes)
|
||||
export const FileTreeSelectableProvider: FC<{
|
||||
onSelect: (value: FindResult[]) => void
|
||||
}> = ({ onSelect, children }) => {
|
||||
const { _id: projectId, rootDocId } = useProjectContext()
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
|
||||
const [initialSelectedEntityId] = usePersistedState(
|
||||
`doc.open_id.${projectId}`,
|
||||
|
@ -98,7 +140,7 @@ export function FileTreeSelectableProvider({ onSelect, children }) {
|
|||
: fileTreeSelectableReadWriteReducer,
|
||||
null,
|
||||
() => {
|
||||
if (!initialSelectedEntityId) return new Set()
|
||||
if (!initialSelectedEntityId) return new Set<string>()
|
||||
|
||||
// the entity with id=initialSelectedEntityId might not exist in the tree
|
||||
// anymore. This checks that it exists before initialising the reducer
|
||||
|
@ -107,21 +149,21 @@ export function FileTreeSelectableProvider({ onSelect, children }) {
|
|||
return new Set([initialSelectedEntityId])
|
||||
|
||||
// the entity doesn't exist anymore; don't select any files
|
||||
return new Set()
|
||||
return new Set<string>()
|
||||
}
|
||||
)
|
||||
|
||||
const [selectedEntityParentIds, setSelectedEntityParentIds] = useState(
|
||||
new Set()
|
||||
)
|
||||
const [selectedEntityParentIds, setSelectedEntityParentIds] = useState<
|
||||
Set<string>
|
||||
>(new Set())
|
||||
|
||||
// fills `selectedEntityParentIds` set
|
||||
useEffect(() => {
|
||||
const ids = new Set()
|
||||
const ids = new Set<string>()
|
||||
selectedEntityIds.forEach(id => {
|
||||
const found = findInTree(fileTreeData, id)
|
||||
if (found) {
|
||||
found.path.forEach(pathItem => ids.add(pathItem))
|
||||
found.path.forEach((pathItem: any) => ids.add(pathItem))
|
||||
}
|
||||
})
|
||||
setSelectedEntityParentIds(ids)
|
||||
|
@ -148,7 +190,7 @@ export function FileTreeSelectableProvider({ onSelect, children }) {
|
|||
|
||||
useEffect(() => {
|
||||
// listen for `editor.openDoc` and selected that doc
|
||||
function handleOpenDoc(ev) {
|
||||
function handleOpenDoc(ev: any) {
|
||||
const found = findInTree(fileTreeData, ev.detail)
|
||||
if (!found) return
|
||||
|
||||
|
@ -175,6 +217,7 @@ export function FileTreeSelectableProvider({ onSelect, children }) {
|
|||
dispatch({ type: actionType, id })
|
||||
}, [])
|
||||
|
||||
// TODO: wrap in useMemo
|
||||
const value = {
|
||||
selectedEntityIds,
|
||||
selectedEntityParentIds,
|
||||
|
@ -192,26 +235,9 @@ export function FileTreeSelectableProvider({ onSelect, children }) {
|
|||
)
|
||||
}
|
||||
|
||||
FileTreeSelectableProvider.propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
}
|
||||
|
||||
const projectContextPropTypes = {
|
||||
_id: PropTypes.string.isRequired,
|
||||
rootDocId: PropTypes.string,
|
||||
}
|
||||
|
||||
const editorContextPropTypes = {
|
||||
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
|
||||
}
|
||||
|
||||
const isMac = /Mac/.test(window.navigator?.platform)
|
||||
|
||||
export function useSelectableEntity(id, type) {
|
||||
export function useSelectableEntity(id: string, type: string) {
|
||||
const { view, setView } = useLayoutContext()
|
||||
const { setContextMenuCoords } = useFileTreeMainContext()
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
|
@ -220,7 +246,7 @@ export function useSelectableEntity(id, type) {
|
|||
selectOrMultiSelectEntity,
|
||||
isRootFolderSelected,
|
||||
setIsRootFolderSelected,
|
||||
} = useContext(FileTreeSelectableContext)
|
||||
} = useFileTreeSelectable()
|
||||
|
||||
const isSelected = selectedEntityIds.has(id)
|
||||
|
||||
|
@ -369,9 +395,10 @@ export function useFileTreeSelectable() {
|
|||
return context
|
||||
}
|
||||
|
||||
const alphabetical = (a, b) => fileCollator.compare(a.name, b.name)
|
||||
const alphabetical = (a: FileTreeEntity, b: FileTreeEntity) =>
|
||||
fileCollator.compare(a.name, b.name)
|
||||
|
||||
function* sortedItems(folder) {
|
||||
function* sortedItems(folder: Folder): Generator<string> {
|
||||
yield folder._id
|
||||
|
||||
const folders = [...folder.folders].sort(alphabetical)
|
|
@ -110,8 +110,14 @@ App.controller('ReactFileTreeController', [
|
|||
|
||||
App.component(
|
||||
'fileTreeRoot',
|
||||
react2angular(
|
||||
rootContext.use(FileTreeRoot),
|
||||
Object.keys(FileTreeRoot.propTypes)
|
||||
)
|
||||
react2angular(rootContext.use(FileTreeRoot), [
|
||||
'onSelect',
|
||||
'onDelete',
|
||||
'onInit',
|
||||
'isConnected',
|
||||
'setRefProviderEnabled',
|
||||
'setStartedFreeTrial',
|
||||
'reindexReferences',
|
||||
'refProviders',
|
||||
])
|
||||
)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useCallback, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
|
||||
|
@ -7,10 +6,8 @@ import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
|
|||
import { findInTree, findInTreeOrThrow } from '../util/find-in-tree'
|
||||
import { useIdeContext } from '@/shared/context/ide-context'
|
||||
|
||||
export function useFileTreeSocketListener(onDelete) {
|
||||
const user = useUserContext({
|
||||
id: PropTypes.string.isRequired,
|
||||
})
|
||||
export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
|
||||
const user = useUserContext()
|
||||
const {
|
||||
dispatchRename,
|
||||
dispatchDelete,
|
||||
|
@ -41,7 +38,7 @@ export function useFileTreeSocketListener(onDelete) {
|
|||
)
|
||||
|
||||
useEffect(() => {
|
||||
function handleDispatchRename(entityId, name) {
|
||||
function handleDispatchRename(entityId: string, name: string) {
|
||||
dispatchRename(entityId, name)
|
||||
}
|
||||
if (socket) socket.on('reciveEntityRename', handleDispatchRename)
|
||||
|
@ -52,7 +49,7 @@ export function useFileTreeSocketListener(onDelete) {
|
|||
}, [socket, dispatchRename])
|
||||
|
||||
useEffect(() => {
|
||||
function handleDispatchDelete(entityId) {
|
||||
function handleDispatchDelete(entityId: string) {
|
||||
const entity = findInTree(fileTreeData, entityId)
|
||||
unselect(entityId)
|
||||
if (selectedEntityParentIds.has(entityId)) {
|
||||
|
@ -88,7 +85,7 @@ export function useFileTreeSocketListener(onDelete) {
|
|||
])
|
||||
|
||||
useEffect(() => {
|
||||
function handleDispatchMove(entityId, toFolderId) {
|
||||
function handleDispatchMove(entityId: string, toFolderId: string) {
|
||||
dispatchMove(entityId, toFolderId)
|
||||
}
|
||||
if (socket) socket.on('reciveEntityMove', handleDispatchMove)
|
||||
|
@ -98,7 +95,7 @@ export function useFileTreeSocketListener(onDelete) {
|
|||
}, [socket, dispatchMove])
|
||||
|
||||
useEffect(() => {
|
||||
function handleDispatchCreateFolder(parentFolderId, folder, userId) {
|
||||
function handleDispatchCreateFolder(parentFolderId: string, folder: any) {
|
||||
dispatchCreateFolder(parentFolderId, folder)
|
||||
}
|
||||
if (socket) socket.on('reciveNewFolder', handleDispatchCreateFolder)
|
||||
|
@ -109,7 +106,11 @@ export function useFileTreeSocketListener(onDelete) {
|
|||
}, [socket, dispatchCreateFolder])
|
||||
|
||||
useEffect(() => {
|
||||
function handleDispatchCreateDoc(parentFolderId, doc, _source, userId) {
|
||||
function handleDispatchCreateDoc(
|
||||
parentFolderId: string,
|
||||
doc: any,
|
||||
_source: unknown
|
||||
) {
|
||||
dispatchCreateDoc(parentFolderId, doc)
|
||||
}
|
||||
if (socket) socket.on('reciveNewDoc', handleDispatchCreateDoc)
|
||||
|
@ -120,11 +121,11 @@ export function useFileTreeSocketListener(onDelete) {
|
|||
|
||||
useEffect(() => {
|
||||
function handleDispatchCreateFile(
|
||||
parentFolderId,
|
||||
file,
|
||||
_source,
|
||||
linkedFileData,
|
||||
userId
|
||||
parentFolderId: string,
|
||||
file: any,
|
||||
_source: unknown,
|
||||
linkedFileData: any,
|
||||
userId: string
|
||||
) {
|
||||
dispatchCreateFile(parentFolderId, file)
|
||||
if (linkedFileData) {
|
|
@ -1,5 +1,4 @@
|
|||
import { useState, type ElementType } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
import Icon from '../../../shared/components/icon'
|
||||
|
@ -48,12 +47,8 @@ type FileViewHeaderProps = {
|
|||
}
|
||||
|
||||
export default function FileViewHeader({ file }: FileViewHeaderProps) {
|
||||
const { _id: projectId } = useProjectContext({
|
||||
_id: PropTypes.string.isRequired,
|
||||
})
|
||||
const { permissionsLevel } = useEditorContext({
|
||||
permissionsLevel: PropTypes.string,
|
||||
})
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [refreshError, setRefreshError] = useState<Nullable<string>>(null)
|
||||
|
@ -150,7 +145,6 @@ function ProjectFilePathProvider({ file }: ProjectFilePathProviderProps) {
|
|||
return (
|
||||
<p>
|
||||
<LinkedFileIcon />
|
||||
|
||||
<Trans
|
||||
i18nKey="imported_from_another_project_at_date"
|
||||
components={
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
|
||||
export default function FileViewImage({ fileName, fileId, onLoad, onError }) {
|
||||
const { _id: projectId } = useProjectContext({
|
||||
_id: PropTypes.string.isRequired,
|
||||
})
|
||||
|
||||
return (
|
||||
<img
|
||||
src={`/project/${projectId}/file/${fileId}`}
|
||||
onLoad={onLoad}
|
||||
onError={onError}
|
||||
alt={fileName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
FileViewImage.propTypes = {
|
||||
fileName: PropTypes.string.isRequired,
|
||||
fileId: PropTypes.string.isRequired,
|
||||
onLoad: PropTypes.func.isRequired,
|
||||
onError: PropTypes.func.isRequired,
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
|
||||
export default function FileViewImage({
|
||||
fileName,
|
||||
fileId,
|
||||
onLoad,
|
||||
onError,
|
||||
}: {
|
||||
fileName: string
|
||||
fileId: string
|
||||
onLoad: () => void
|
||||
onError: () => void
|
||||
}) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
return (
|
||||
<img
|
||||
src={`/project/${projectId}/file/${fileId}`}
|
||||
onLoad={onLoad}
|
||||
onError={onError}
|
||||
alt={fileName}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -1,15 +1,20 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import useAbortController from '../../../shared/hooks/use-abort-controller'
|
||||
|
||||
const MAX_FILE_SIZE = 2 * 1024 * 1024
|
||||
|
||||
export default function FileViewText({ file, onLoad, onError }) {
|
||||
const { _id: projectId } = useProjectContext({
|
||||
_id: PropTypes.string.isRequired,
|
||||
})
|
||||
export default function FileViewText({
|
||||
file,
|
||||
onLoad,
|
||||
onError,
|
||||
}: {
|
||||
file: { id: string }
|
||||
onLoad: () => void
|
||||
onError: () => void
|
||||
}) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
const [textPreview, setTextPreview] = useState('')
|
||||
const [shouldShowDots, setShouldShowDots] = useState(false)
|
||||
|
@ -27,7 +32,7 @@ export default function FileViewText({ file, onLoad, onError }) {
|
|||
() => fetchContentLengthController.abort(),
|
||||
10000
|
||||
)
|
||||
let fetchDataTimeout
|
||||
let fetchDataTimeout: number | undefined
|
||||
fetch(path, { method: 'HEAD', signal: fetchContentLengthController.signal })
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error('HTTP Error Code: ' + response.status)
|
||||
|
@ -36,7 +41,7 @@ export default function FileViewText({ file, onLoad, onError }) {
|
|||
.then(fileSize => {
|
||||
let truncated = false
|
||||
let maxSize = null
|
||||
if (fileSize > MAX_FILE_SIZE) {
|
||||
if (fileSize && Number(fileSize) > MAX_FILE_SIZE) {
|
||||
truncated = true
|
||||
maxSize = MAX_FILE_SIZE
|
||||
}
|
||||
|
@ -44,7 +49,10 @@ export default function FileViewText({ file, onLoad, onError }) {
|
|||
if (maxSize != null) {
|
||||
path += `?range=0-${maxSize}`
|
||||
}
|
||||
fetchDataTimeout = setTimeout(() => fetchDataController.abort(), 60000)
|
||||
fetchDataTimeout = window.setTimeout(
|
||||
() => fetchDataController.abort(),
|
||||
60000
|
||||
)
|
||||
return fetch(path, { signal: fetchDataController.signal }).then(
|
||||
response => {
|
||||
return response.text().then(text => {
|
||||
|
@ -90,9 +98,3 @@ export default function FileViewText({ file, onLoad, onError }) {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
FileViewText.propTypes = {
|
||||
file: PropTypes.shape({ id: PropTypes.string }).isRequired,
|
||||
onLoad: PropTypes.func.isRequired,
|
||||
onError: PropTypes.func.isRequired,
|
||||
}
|
|
@ -16,11 +16,6 @@ import useAsync from '@/shared/hooks/use-async'
|
|||
import { completeHistoryTutorial } from '../../services/api'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
type EditorTutorials = {
|
||||
inactiveTutorials: [string]
|
||||
deactivateTutorial: (key: string) => void
|
||||
}
|
||||
|
||||
function AllHistoryList() {
|
||||
const { id: currentUserId } = useUserContext()
|
||||
const {
|
||||
|
@ -97,8 +92,7 @@ function AllHistoryList() {
|
|||
}
|
||||
}, [updatesLoadingState])
|
||||
|
||||
const { inactiveTutorials, deactivateTutorial }: EditorTutorials =
|
||||
useEditorContext()
|
||||
const { inactiveTutorials, deactivateTutorial } = useEditorContext()
|
||||
|
||||
const [showPopover, setShowPopover] = useState(() => {
|
||||
// only show tutorial popover if they haven't dismissed ("completed") it yet
|
||||
|
|
|
@ -22,7 +22,6 @@ function EditorNavigationToolbar() {
|
|||
return (
|
||||
<>
|
||||
<EditorNavigationToolbarRoot
|
||||
// @ts-ignore
|
||||
onlineUsersArray={onlineUsersArray}
|
||||
openDoc={openDoc}
|
||||
openShareProjectModal={handleOpenShareModal}
|
||||
|
|
|
@ -91,7 +91,7 @@ export const FileTreeOpenProvider: FC = ({ children }) => {
|
|||
eventEmitter.emit('entity:deleted', entity)
|
||||
// Select the root document if the current document was deleted
|
||||
if (entity.entity._id === openDocId) {
|
||||
openDocWithId(rootDocId)
|
||||
openDocWithId(rootDocId!)
|
||||
}
|
||||
},
|
||||
[eventEmitter, openDocId, openDocWithId, rootDocId]
|
||||
|
@ -116,7 +116,12 @@ export const FileTreeOpenProvider: FC = ({ children }) => {
|
|||
// Open a document once the file tree and project are ready
|
||||
const initialOpenDoneRef = useRef(false)
|
||||
useEffect(() => {
|
||||
if (fileTreeReady && projectJoined && !initialOpenDoneRef.current) {
|
||||
if (
|
||||
rootDocId &&
|
||||
fileTreeReady &&
|
||||
projectJoined &&
|
||||
!initialOpenDoneRef.current
|
||||
) {
|
||||
initialOpenDoneRef.current = true
|
||||
openInitialDoc(rootDocId)
|
||||
}
|
||||
|
|
|
@ -523,7 +523,8 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
if (!user) {
|
||||
return 'anonymous'
|
||||
}
|
||||
if (project.owner === user.id) {
|
||||
// FIXME: check this
|
||||
if (project.owner._id === user.id) {
|
||||
return 'member'
|
||||
}
|
||||
for (const member of project.members as any[]) {
|
||||
|
@ -711,7 +712,7 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
)
|
||||
|
||||
const applyTrackChangesStateToClient = useCallback(
|
||||
(state: boolean | Record<UserId, boolean>) => {
|
||||
(state: boolean | ReviewPanel.Value<'trackChangesState'>) => {
|
||||
if (typeof state === 'boolean') {
|
||||
setEveryoneTCState(state)
|
||||
setGuestsTCState(state)
|
||||
|
@ -728,13 +729,13 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
newTrackChangesState = setUserTCState(
|
||||
newTrackChangesState,
|
||||
member._id,
|
||||
state[member._id] ?? false
|
||||
!!state[member._id]
|
||||
)
|
||||
}
|
||||
newTrackChangesState = setUserTCState(
|
||||
newTrackChangesState,
|
||||
project.owner._id,
|
||||
state[project.owner._id] ?? false
|
||||
!!state[project.owner._id]
|
||||
)
|
||||
return newTrackChangesState
|
||||
}
|
||||
|
@ -750,7 +751,7 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
)
|
||||
|
||||
const setGuestFeatureBasedOnProjectAccessLevel = (
|
||||
projectPublicAccessLevel: PublicAccessLevel
|
||||
projectPublicAccessLevel?: PublicAccessLevel
|
||||
) => {
|
||||
setTrackChangesForGuestsAvailable(projectPublicAccessLevel === 'tokenBased')
|
||||
}
|
||||
|
@ -1169,7 +1170,7 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
|
|||
}, [currentDocumentId, getDocEntries, trackChangesVisible])
|
||||
|
||||
useEffect(() => {
|
||||
setMiniReviewPanelVisible(!reviewPanelOpen && hasEntries)
|
||||
setMiniReviewPanelVisible(!reviewPanelOpen && !!hasEntries)
|
||||
}, [reviewPanelOpen, hasEntries, setMiniReviewPanelVisible])
|
||||
|
||||
// listen for events from the CodeMirror 6 track changes extension
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
|
@ -8,7 +7,18 @@ import Icon from '../../../shared/components/icon'
|
|||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
import Tooltip from '../../../shared/components/tooltip'
|
||||
|
||||
const OutlinePane = React.memo(function OutlinePane({
|
||||
const OutlinePane = React.memo<{
|
||||
isTexFile: boolean
|
||||
outline: any[]
|
||||
jumpToLine(line: number): void
|
||||
onToggle(value: boolean): void
|
||||
eventTracking: any
|
||||
highlightedLine?: number
|
||||
show: boolean
|
||||
isPartial?: boolean
|
||||
expanded?: boolean
|
||||
toggleExpanded: () => void
|
||||
}>(function OutlinePane({
|
||||
isTexFile,
|
||||
outline,
|
||||
jumpToLine,
|
||||
|
@ -20,7 +30,7 @@ const OutlinePane = React.memo(function OutlinePane({
|
|||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isOpen = isTexFile && expanded
|
||||
const isOpen = Boolean(isTexFile && expanded)
|
||||
|
||||
useEffect(() => {
|
||||
onToggle(isOpen)
|
||||
|
@ -73,15 +83,4 @@ const OutlinePane = React.memo(function OutlinePane({
|
|||
)
|
||||
})
|
||||
|
||||
OutlinePane.propTypes = {
|
||||
isTexFile: PropTypes.bool.isRequired,
|
||||
outline: PropTypes.array.isRequired,
|
||||
jumpToLine: PropTypes.func.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
highlightedLine: PropTypes.number,
|
||||
isPartial: PropTypes.bool,
|
||||
expanded: PropTypes.bool,
|
||||
toggleExpanded: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default withErrorBoundary(OutlinePane)
|
|
@ -5,7 +5,7 @@ import { FC } from 'react'
|
|||
|
||||
export const CompileTimeoutWarning: FC<{
|
||||
handleDismissWarning: () => void
|
||||
showNewCompileTimeoutUI: string
|
||||
showNewCompileTimeoutUI?: string
|
||||
}> = ({ handleDismissWarning, showNewCompileTimeoutUI }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
|
|
@ -26,8 +26,8 @@ function FasterCompilesFeedbackContent() {
|
|||
true
|
||||
)
|
||||
const [sayThanks, setSayThanks] = useState(false)
|
||||
const lastClsiServerId = useRef('')
|
||||
const lastPdfUrl = useRef('')
|
||||
const lastClsiServerId = useRef<string | undefined>(undefined)
|
||||
const lastPdfUrl = useRef<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
@ -52,7 +52,7 @@ function FasterCompilesFeedbackContent() {
|
|||
projectId,
|
||||
server: clsiServerId?.includes('-c2d-') ? 'faster' : 'normal',
|
||||
feedback,
|
||||
pdfSize: pdfFile.size,
|
||||
pdfSize: pdfFile?.size,
|
||||
...deliveryLatencies,
|
||||
})
|
||||
setHasRatedProject(true)
|
||||
|
|
|
@ -136,7 +136,7 @@ type ReadOrWriteFormGroupProps = {
|
|||
id: string
|
||||
type: string
|
||||
label: string
|
||||
value: string
|
||||
value?: string
|
||||
handleChange: (event: any) => void
|
||||
canEdit: boolean
|
||||
required: boolean
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useUserContext } from '../../../shared/context/user-context'
|
||||
|
||||
|
@ -13,14 +12,11 @@ import { useSplitTestContext } from '../../../shared/context/split-test-context'
|
|||
|
||||
export default function AddCollaboratorsUpgrade() {
|
||||
const { t } = useTranslation()
|
||||
const user = useUserContext({
|
||||
allowedFreeTrial: PropTypes.bool,
|
||||
})
|
||||
const user = useUserContext()
|
||||
|
||||
const [startedFreeTrial, setStartedFreeTrial] = useState(false)
|
||||
const { splitTestVariants } = useSplitTestContext({
|
||||
splitTestVariants: PropTypes.object,
|
||||
})
|
||||
const { splitTestVariants } = useSplitTestContext()
|
||||
|
||||
const variant = splitTestVariants['project-share-modal-paywall']
|
||||
|
||||
return (
|
|
@ -10,13 +10,10 @@ import { useProjectContext } from '../../../shared/context/project-context'
|
|||
import { useSplitTestContext } from '../../../shared/context/split-test-context'
|
||||
import { useMemo } from 'react'
|
||||
import { Row } from 'react-bootstrap'
|
||||
import PropTypes from 'prop-types'
|
||||
import RecaptchaConditions from '../../../shared/components/recaptcha-conditions'
|
||||
|
||||
export default function ShareModalBody() {
|
||||
const { splitTestVariants } = useSplitTestContext({
|
||||
splitTestVariants: PropTypes.object,
|
||||
})
|
||||
const { splitTestVariants } = useSplitTestContext()
|
||||
|
||||
const { members, invites, features } = useProjectContext()
|
||||
const { isProjectOwner } = useEditorContext()
|
||||
|
@ -32,7 +29,7 @@ export default function ShareModalBody() {
|
|||
return true
|
||||
}
|
||||
|
||||
return members.length + invites.length < features.collaborators
|
||||
return members.length + invites.length < (features.collaborators ?? 1)
|
||||
}, [members, invites, features, isProjectOwner])
|
||||
|
||||
switch (splitTestVariants['project-share-modal-paywall']) {
|
|
@ -5,17 +5,14 @@ import React, {
|
|||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ShareProjectModalContent from './share-project-modal-content'
|
||||
import {
|
||||
useProjectContext,
|
||||
projectShape,
|
||||
} from '../../../shared/context/project-context'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import { useSplitTestContext } from '../../../shared/context/split-test-context'
|
||||
import { sendMB } from '../../../infrastructure/event-tracking'
|
||||
import { Project } from '../../../../../types/project'
|
||||
|
||||
type ShareProjectContextValue = {
|
||||
updateProject: (data: unknown) => void
|
||||
updateProject: (project: Project) => void
|
||||
monitorRequest: <T extends Promise<unknown>>(request: () => T) => T
|
||||
inFlight: boolean
|
||||
setInFlight: React.Dispatch<
|
||||
|
@ -58,11 +55,9 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
|
|||
useState<ShareProjectContextValue['inFlight']>(false)
|
||||
const [error, setError] = useState<ShareProjectContextValue['error']>()
|
||||
|
||||
const project = useProjectContext(projectShape)
|
||||
const project = useProjectContext()
|
||||
|
||||
const { splitTestVariants } = useSplitTestContext({
|
||||
splitTestVariants: PropTypes.object,
|
||||
})
|
||||
const { splitTestVariants } = useSplitTestContext()
|
||||
|
||||
// send tracking event when the modal is opened
|
||||
useEffect(() => {
|
||||
|
|
|
@ -6,12 +6,11 @@ import Icon from '../../../../shared/components/icon'
|
|||
import OverviewFile from './overview-file'
|
||||
import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
import { MainDocument } from '../../../../../../types/project-settings'
|
||||
import { memo } from 'react'
|
||||
|
||||
function OverviewContainer() {
|
||||
const { isOverviewLoading } = useReviewPanelValueContext()
|
||||
const docs: MainDocument[] = useFileTreeData().docs
|
||||
const { docs } = useFileTreeData()
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
|
|
@ -13,10 +13,7 @@ import {
|
|||
ThreadId,
|
||||
} from '../../../../../../../types/review-panel/review-panel'
|
||||
import { ReviewPanelResolvedCommentThread } from '../../../../../../../types/review-panel/comment-thread'
|
||||
import {
|
||||
DocId,
|
||||
MainDocument,
|
||||
} from '../../../../../../../types/project-settings'
|
||||
import { DocId } from '../../../../../../../types/project-settings'
|
||||
import { ReviewPanelEntry } from '../../../../../../../types/review-panel/entry'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
|
||||
|
@ -35,7 +32,7 @@ function ResolvedCommentsDropdown() {
|
|||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { commentThreads, resolvedComments, permissions } =
|
||||
useReviewPanelValueContext()
|
||||
const docs: MainDocument[] = useFileTreeData().docs
|
||||
const { docs } = useFileTreeData()
|
||||
|
||||
const { refreshResolvedCommentsDropdown } = useReviewPanelUpdaterFnsContext()
|
||||
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useEditorContext } from '../../../../../shared/context/editor-context'
|
||||
import { Button, Overlay, Popover } from 'react-bootstrap'
|
||||
import Close from '../../../../../shared/components/close'
|
||||
|
@ -19,7 +18,6 @@ import { useSplitTestContext } from '../../../../../shared/context/split-test-co
|
|||
import { User } from '../../../../../../../types/user'
|
||||
import { useUserContext } from '../../../../../shared/context/user-context'
|
||||
import grammarlyExtensionPresent from '../../../../../shared/utils/grammarly'
|
||||
import { EditorTutorials } from '../../../../../../../types/tutorial'
|
||||
import { debugConsole } from '../../../../../utils/debugging'
|
||||
|
||||
const DELAY_BEFORE_SHOWING_PROMOTION = 1000
|
||||
|
@ -27,19 +25,11 @@ const NEW_USER_CUTOFF_TIME = new Date(2023, 8, 20).getTime()
|
|||
const NOW_TIME = new Date().getTime()
|
||||
const GRAMMARLY_CUTOFF_TIME = new Date(2023, 9, 10).getTime()
|
||||
|
||||
const editorContextPropTypes = {
|
||||
inactiveTutorials: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
deactivateTutorial: PropTypes.func.isRequired,
|
||||
currentPopup: PropTypes.string,
|
||||
setCurrentPopup: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export const PromotionOverlay: FC = ({ children }) => {
|
||||
const ref = useRef<HTMLSpanElement>(null)
|
||||
|
||||
const { inactiveTutorials, currentPopup, setCurrentPopup }: EditorTutorials =
|
||||
useEditorContext(editorContextPropTypes)
|
||||
|
||||
const { inactiveTutorials, currentPopup, setCurrentPopup } =
|
||||
useEditorContext()
|
||||
const {
|
||||
splitTestVariants,
|
||||
}: { splitTestVariants: Record<string, string | undefined> } =
|
||||
|
@ -88,9 +78,7 @@ const PromotionOverlayContent = memo(
|
|||
_props,
|
||||
ref: Ref<HTMLSpanElement>
|
||||
) {
|
||||
const { deactivateTutorial }: EditorTutorials = useEditorContext(
|
||||
editorContextPropTypes
|
||||
)
|
||||
const { deactivateTutorial } = useEditorContext()
|
||||
const [timeoutExpired, setTimeoutExpired] = useState(false)
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import { createContext, useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
useLocalCompileContext,
|
||||
CompileContextPropTypes,
|
||||
} from './local-compile-context'
|
||||
import { createContext, FC, useContext, useMemo } from 'react'
|
||||
import { CompileContext, useLocalCompileContext } from './local-compile-context'
|
||||
import useDetachStateWatcher from '../hooks/use-detach-state-watcher'
|
||||
import useDetachAction from '../hooks/use-detach-action'
|
||||
import useCompileTriggers from '../../features/pdf-preview/hooks/use-compile-triggers'
|
||||
|
||||
export const DetachCompileContext = createContext()
|
||||
export const DetachCompileContext = createContext<CompileContext | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
DetachCompileContext.Provider.propTypes = CompileContextPropTypes
|
||||
|
||||
export function DetachCompileProvider({ children }) {
|
||||
export const DetachCompileProvider: FC = ({ children }) => {
|
||||
const localCompileContext = useLocalCompileContext()
|
||||
if (!localCompileContext) {
|
||||
throw new Error(
|
||||
|
@ -435,6 +431,7 @@ export function DetachCompileProvider({ children }) {
|
|||
validationIssues,
|
||||
firstRenderDone,
|
||||
setChangedAt,
|
||||
setSavedAt,
|
||||
cleanupCompileResult,
|
||||
syncToEntry,
|
||||
}),
|
||||
|
@ -488,6 +485,7 @@ export function DetachCompileProvider({ children }) {
|
|||
validationIssues,
|
||||
firstRenderDone,
|
||||
setChangedAt,
|
||||
setSavedAt,
|
||||
cleanupCompileResult,
|
||||
syncToEntry,
|
||||
]
|
||||
|
@ -500,17 +498,12 @@ export function DetachCompileProvider({ children }) {
|
|||
)
|
||||
}
|
||||
|
||||
DetachCompileProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
export function useDetachCompileContext(propTypes) {
|
||||
const data = useContext(DetachCompileContext)
|
||||
PropTypes.checkPropTypes(
|
||||
propTypes,
|
||||
data,
|
||||
'data',
|
||||
'DetachCompileContext.Provider'
|
||||
)
|
||||
return data
|
||||
export function useDetachCompileContext() {
|
||||
const context = useContext(DetachCompileContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useDetachCompileContext is ony available inside DetachCompileProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
|
@ -5,25 +5,32 @@ import {
|
|||
useMemo,
|
||||
useEffect,
|
||||
useState,
|
||||
FC,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import getMeta from '../../utils/meta'
|
||||
import { buildUrlWithDetachRole } from '../utils/url-helper'
|
||||
import useCallbackHandlers from '../hooks/use-callback-handlers'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
export const DetachContext = createContext()
|
||||
export type DetachRole = 'detacher' | 'detached' | null
|
||||
|
||||
DetachContext.Provider.propTypes = {
|
||||
value: PropTypes.shape({
|
||||
role: PropTypes.oneOf(['detacher', 'detached', null]),
|
||||
setRole: PropTypes.func.isRequired,
|
||||
broadcastEvent: PropTypes.func.isRequired,
|
||||
addEventHandler: PropTypes.func.isRequired,
|
||||
deleteEventHandler: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
type Message = {
|
||||
role: DetachRole
|
||||
event: string
|
||||
data?: any
|
||||
}
|
||||
|
||||
export const DetachContext = createContext<
|
||||
| {
|
||||
role: DetachRole
|
||||
setRole: (role: DetachRole) => void
|
||||
broadcastEvent: (event: string, data?: any) => void
|
||||
addEventHandler: (handler: (...args: any[]) => void) => void
|
||||
deleteEventHandler: (handler: (...args: any[]) => void) => void
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
const debugPdfDetach = getMeta('ol-debugPdfDetach')
|
||||
|
||||
const projectId = getMeta('ol-project_id')
|
||||
|
@ -33,8 +40,8 @@ export const detachChannel =
|
|||
? new BroadcastChannel(detachChannelId)
|
||||
: undefined
|
||||
|
||||
export function DetachProvider({ children }) {
|
||||
const [lastDetachedConnectedAt, setLastDetachedConnectedAt] = useState()
|
||||
export const DetachProvider: FC = ({ children }) => {
|
||||
const [lastDetachedConnectedAt, setLastDetachedConnectedAt] = useState<Date>()
|
||||
const [role, setRole] = useState(() => getMeta('ol-detachRole') || null)
|
||||
const {
|
||||
addHandler: addEventHandler,
|
||||
|
@ -51,7 +58,7 @@ export function DetachProvider({ children }) {
|
|||
|
||||
useEffect(() => {
|
||||
if (detachChannel) {
|
||||
const listener = event => {
|
||||
const listener = (event: MessageEvent) => {
|
||||
if (debugPdfDetach) {
|
||||
debugConsole.warn(`Receiving:`, event.data)
|
||||
}
|
||||
|
@ -67,7 +74,7 @@ export function DetachProvider({ children }) {
|
|||
}, [callEventHandlers])
|
||||
|
||||
const broadcastEvent = useCallback(
|
||||
(event, data) => {
|
||||
(event: string, data?: any) => {
|
||||
if (!role) {
|
||||
if (debugPdfDetach) {
|
||||
debugConsole.warn('Not Broadcasting (no role)', {
|
||||
|
@ -85,10 +92,7 @@ export function DetachProvider({ children }) {
|
|||
data,
|
||||
})
|
||||
}
|
||||
const message = {
|
||||
role,
|
||||
event,
|
||||
}
|
||||
const message: Message = { role, event }
|
||||
if (data) {
|
||||
message.data = data
|
||||
}
|
||||
|
@ -109,7 +113,7 @@ export function DetachProvider({ children }) {
|
|||
}, [broadcastEvent])
|
||||
|
||||
useEffect(() => {
|
||||
const updateLastDetachedConnectedAt = message => {
|
||||
const updateLastDetachedConnectedAt = (message: Message) => {
|
||||
if (message.role === 'detached' && message.event === 'connected') {
|
||||
setLastDetachedConnectedAt(new Date())
|
||||
}
|
||||
|
@ -142,15 +146,10 @@ export function DetachProvider({ children }) {
|
|||
)
|
||||
}
|
||||
|
||||
DetachProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
export function useDetachContext(propTypes) {
|
||||
export function useDetachContext() {
|
||||
const data = useContext(DetachContext)
|
||||
if (!data) {
|
||||
throw new Error('useDetachContext is only available inside DetachProvider')
|
||||
}
|
||||
PropTypes.checkPropTypes(propTypes, data, 'data', 'DetachContext.Provider')
|
||||
return data
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
import {
|
||||
createContext,
|
||||
Dispatch,
|
||||
FC,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import useScopeValue from '../hooks/use-scope-value'
|
||||
import useBrowserWindow from '../hooks/use-browser-window'
|
||||
import { useIdeContext } from './ide-context'
|
||||
|
@ -15,56 +17,49 @@ import { useDetachContext } from './detach-context'
|
|||
import getMeta from '../../utils/meta'
|
||||
import { useUserContext } from './user-context'
|
||||
import { saveProjectSettings } from '@/features/editor-left-menu/utils/api'
|
||||
import { PermissionsLevel } from '@/features/ide-react/types/permissions'
|
||||
|
||||
export const EditorContext = createContext()
|
||||
export const EditorContext = createContext<
|
||||
| {
|
||||
cobranding?: {
|
||||
logoImgUrl: string
|
||||
brandVariationName: string
|
||||
brandVariationId: number
|
||||
brandId: number
|
||||
brandVariationHomeUrl: string
|
||||
publishGuideHtml?: string
|
||||
partner?: string
|
||||
brandedMenu?: boolean
|
||||
submitBtnHtml?: string
|
||||
}
|
||||
hasPremiumCompile?: boolean
|
||||
loading?: boolean
|
||||
renameProject: (newName: string) => void
|
||||
setPermissionsLevel: (permissionsLevel: PermissionsLevel) => void
|
||||
showSymbolPalette?: boolean
|
||||
toggleSymbolPalette?: () => void
|
||||
insertSymbol?: (symbol: string) => void
|
||||
isProjectOwner: boolean
|
||||
isRestrictedTokenMember?: boolean
|
||||
permissionsLevel: 'readOnly' | 'readAndWrite' | 'owner'
|
||||
deactivateTutorial: (tutorial: string) => void
|
||||
inactiveTutorials: [string]
|
||||
currentPopup: string | null
|
||||
setCurrentPopup: Dispatch<SetStateAction<string | null>>
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
EditorContext.Provider.propTypes = {
|
||||
value: PropTypes.shape({
|
||||
cobranding: PropTypes.shape({
|
||||
logoImgUrl: PropTypes.string.isRequired,
|
||||
brandVariationName: PropTypes.string.isRequired,
|
||||
brandVariationId: PropTypes.number.isRequired,
|
||||
brandId: PropTypes.number.isRequired,
|
||||
brandVariationHomeUrl: PropTypes.string.isRequired,
|
||||
publishGuideHtml: PropTypes.string,
|
||||
partner: PropTypes.string,
|
||||
brandedMenu: PropTypes.bool,
|
||||
submitBtnHtml: PropTypes.string,
|
||||
}),
|
||||
hasPremiumCompile: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
renameProject: PropTypes.func.isRequired,
|
||||
showSymbolPalette: PropTypes.bool,
|
||||
toggleSymbolPalette: PropTypes.func,
|
||||
insertSymbol: PropTypes.func,
|
||||
isProjectOwner: PropTypes.bool,
|
||||
isRestrictedTokenMember: PropTypes.bool,
|
||||
permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
|
||||
}),
|
||||
}
|
||||
|
||||
export function EditorProvider({ children }) {
|
||||
export const EditorProvider: FC = ({ children }) => {
|
||||
const ide = useIdeContext()
|
||||
const { id: userId } = useUserContext()
|
||||
const { role } = useDetachContext()
|
||||
|
||||
const {
|
||||
owner,
|
||||
features,
|
||||
_id: projectId,
|
||||
} = useProjectContext({
|
||||
owner: PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
}),
|
||||
features: PropTypes.shape({
|
||||
compileGroup: PropTypes.string,
|
||||
}),
|
||||
_id: PropTypes.string.isRequired,
|
||||
})
|
||||
const { owner, features, _id: projectId } = useProjectContext()
|
||||
|
||||
const cobranding = useMemo(() => {
|
||||
if (window.brandVariation) {
|
||||
return {
|
||||
return (
|
||||
window.brandVariation && {
|
||||
logoImgUrl: window.brandVariation.logo_url,
|
||||
brandVariationName: window.brandVariation.name,
|
||||
brandVariationId: window.brandVariation.id,
|
||||
|
@ -75,9 +70,7 @@ export function EditorProvider({ children }) {
|
|||
brandedMenu: window.brandVariation.branded_menu,
|
||||
submitBtnHtml: window.brandVariation.submit_button_html,
|
||||
}
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
)
|
||||
}, [])
|
||||
|
||||
const [loading] = useScopeValue('state.loading')
|
||||
|
@ -91,7 +84,7 @@ export function EditorProvider({ children }) {
|
|||
getMeta('ol-inactiveTutorials', [])
|
||||
)
|
||||
|
||||
const [currentPopup, setCurrentPopup] = useState(null)
|
||||
const [currentPopup, setCurrentPopup] = useState<string | null>(null)
|
||||
|
||||
const deactivateTutorial = useCallback(
|
||||
tutorialKey => {
|
||||
|
@ -109,21 +102,26 @@ export function EditorProvider({ children }) {
|
|||
}, [ide?.socket, setProjectName])
|
||||
|
||||
const renameProject = useCallback(
|
||||
newName => {
|
||||
setProjectName(oldName => {
|
||||
(newName: string) => {
|
||||
setProjectName((oldName: string) => {
|
||||
if (oldName !== newName) {
|
||||
saveProjectSettings(projectId, { name: newName }).catch(response => {
|
||||
setProjectName(oldName)
|
||||
const { data, status } = response
|
||||
if (status === 400) {
|
||||
return ide.showGenericMessageModal('Error renaming project', data)
|
||||
} else {
|
||||
return ide.showGenericMessageModal(
|
||||
'Error renaming project',
|
||||
'Please try again in a moment'
|
||||
)
|
||||
saveProjectSettings(projectId, { name: newName }).catch(
|
||||
(response: any) => {
|
||||
setProjectName(oldName)
|
||||
const { data, status } = response
|
||||
if (status === 400) {
|
||||
return ide.showGenericMessageModal(
|
||||
'Error renaming project',
|
||||
data
|
||||
)
|
||||
} else {
|
||||
return ide.showGenericMessageModal(
|
||||
'Error renaming project',
|
||||
'Please try again in a moment'
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return newName
|
||||
})
|
||||
|
@ -152,7 +150,7 @@ export function EditorProvider({ children }) {
|
|||
setTitle(title)
|
||||
}, [projectName, setTitle, role])
|
||||
|
||||
const insertSymbol = useCallback(symbol => {
|
||||
const insertSymbol = useCallback((symbol: string) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('editor:insert-symbol', {
|
||||
detail: symbol,
|
||||
|
@ -201,19 +199,12 @@ export function EditorProvider({ children }) {
|
|||
<EditorContext.Provider value={value}>{children}</EditorContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
EditorProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
export function useEditorContext(propTypes) {
|
||||
export function useEditorContext() {
|
||||
const context = useContext(EditorContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useEditorContext is only available inside EditorProvider')
|
||||
}
|
||||
|
||||
PropTypes.checkPropTypes(propTypes, context, 'data', 'EditorContext.Provider')
|
||||
|
||||
return context
|
||||
}
|
|
@ -5,8 +5,8 @@ import {
|
|||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
FC,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import useScopeValue from '../hooks/use-scope-value'
|
||||
import {
|
||||
renameInTree,
|
||||
|
@ -18,35 +18,70 @@ import { countFiles } from '../../features/file-tree/util/count-in-tree'
|
|||
import useDeepCompareEffect from '../../shared/hooks/use-deep-compare-effect'
|
||||
import { docsInFolder } from '@/features/file-tree/util/docs-in-folder'
|
||||
import useScopeValueSetterOnly from '@/shared/hooks/use-scope-value-setter-only'
|
||||
import { Folder } from '../../../../types/folder'
|
||||
import { Project } from '../../../../types/project'
|
||||
import { MainDocument } from '../../../../types/project-settings'
|
||||
import { FindResult } from '@/features/file-tree/util/path'
|
||||
|
||||
const FileTreeDataContext = createContext()
|
||||
const FileTreeDataContext = createContext<
|
||||
| {
|
||||
// fileTreeData is the up-to-date representation of the files list, updated
|
||||
// by the file tree
|
||||
fileTreeData: Folder
|
||||
fileCount: { value: number; status: string; limit: number } | number
|
||||
hasFolders: boolean
|
||||
selectedEntities: FindResult[]
|
||||
setSelectedEntities: (selectedEntities: FindResult[]) => void
|
||||
dispatchRename: (id: string, name: string) => void
|
||||
dispatchMove: (id: string, target: string) => void
|
||||
dispatchDelete: (id: string) => void
|
||||
dispatchCreateFolder: (name: string, folder: any) => void
|
||||
dispatchCreateDoc: (name: string, doc: any) => void
|
||||
dispatchCreateFile: (name: string, file: any) => void
|
||||
docs?: MainDocument[]
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
const fileTreeDataPropType = PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
docs: PropTypes.array.isRequired,
|
||||
fileRefs: PropTypes.array.isRequired,
|
||||
folders: PropTypes.array.isRequired,
|
||||
})
|
||||
|
||||
FileTreeDataContext.Provider.propTypes = {
|
||||
value: PropTypes.shape({
|
||||
// fileTreeData is the up-to-date representation of the files list, updated
|
||||
// by the file tree
|
||||
fileTreeData: fileTreeDataPropType,
|
||||
hasFolders: PropTypes.bool,
|
||||
}),
|
||||
/* eslint-disable no-unused-vars */
|
||||
enum ACTION_TYPES {
|
||||
RENAME = 'RENAME',
|
||||
RESET = 'RESET',
|
||||
DELETE = 'DELETE',
|
||||
MOVE = 'MOVE',
|
||||
CREATE = 'CREATE',
|
||||
}
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
const ACTION_TYPES = {
|
||||
RENAME: 'RENAME',
|
||||
RESET: 'RESET',
|
||||
DELETE: 'DELETE',
|
||||
MOVE: 'MOVE',
|
||||
CREATE: 'CREATE',
|
||||
}
|
||||
type Action =
|
||||
| {
|
||||
type: ACTION_TYPES.RESET
|
||||
fileTreeData?: Folder
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.RENAME
|
||||
id: string
|
||||
newName: string
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.DELETE
|
||||
id: string
|
||||
}
|
||||
| {
|
||||
type: ACTION_TYPES.MOVE
|
||||
entityId: string
|
||||
toFolderId: string
|
||||
}
|
||||
| {
|
||||
type: typeof ACTION_TYPES.CREATE
|
||||
parentFolderId: string
|
||||
entity: any // TODO
|
||||
}
|
||||
|
||||
function fileTreeMutableReducer({ fileTreeData }, action) {
|
||||
function fileTreeMutableReducer(
|
||||
{ fileTreeData }: { fileTreeData: Folder },
|
||||
action: Action
|
||||
) {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPES.RESET: {
|
||||
const newFileTreeData = action.fileTreeData
|
||||
|
@ -104,12 +139,14 @@ function fileTreeMutableReducer({ fileTreeData }, action) {
|
|||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`Unknown mutable file tree action type: ${action.type}`)
|
||||
throw new Error(
|
||||
`Unknown mutable file tree action type: ${(action as Action).type}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initialState = rootFolder => {
|
||||
const initialState = (rootFolder?: Folder[]) => {
|
||||
const fileTreeData = rootFolder?.[0]
|
||||
return {
|
||||
fileTreeData,
|
||||
|
@ -117,7 +154,7 @@ const initialState = rootFolder => {
|
|||
}
|
||||
}
|
||||
|
||||
export function useFileTreeData(propTypes) {
|
||||
export function useFileTreeData() {
|
||||
const context = useContext(FileTreeDataContext)
|
||||
|
||||
if (!context) {
|
||||
|
@ -126,18 +163,11 @@ export function useFileTreeData(propTypes) {
|
|||
)
|
||||
}
|
||||
|
||||
PropTypes.checkPropTypes(
|
||||
propTypes,
|
||||
context,
|
||||
'data',
|
||||
'FileTreeDataContext.Provider'
|
||||
)
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export function FileTreeDataProvider({ children }) {
|
||||
const [project] = useScopeValue('project')
|
||||
export const FileTreeDataProvider: FC = ({ children }) => {
|
||||
const [project] = useScopeValue<Project>('project')
|
||||
const [openDocId] = useScopeValue('editor.open_doc_id')
|
||||
const [, setOpenDocName] = useScopeValueSetterOnly('editor.open_doc_name')
|
||||
|
||||
|
@ -149,7 +179,7 @@ export function FileTreeDataProvider({ children }) {
|
|||
initialState
|
||||
)
|
||||
|
||||
const [selectedEntities, setSelectedEntities] = useState([])
|
||||
const [selectedEntities, setSelectedEntities] = useState<FindResult[]>([])
|
||||
|
||||
const docs = useMemo(
|
||||
() => (fileTreeData ? docsInFolder(fileTreeData) : undefined),
|
||||
|
@ -172,26 +202,32 @@ export function FileTreeDataProvider({ children }) {
|
|||
})
|
||||
}, [])
|
||||
|
||||
const dispatchCreateDoc = useCallback((parentFolderId, entity) => {
|
||||
entity.type = 'doc'
|
||||
dispatch({
|
||||
type: ACTION_TYPES.CREATE,
|
||||
parentFolderId,
|
||||
entity,
|
||||
})
|
||||
}, [])
|
||||
const dispatchCreateDoc = useCallback(
|
||||
(parentFolderId: string, entity: any) => {
|
||||
entity.type = 'doc'
|
||||
dispatch({
|
||||
type: ACTION_TYPES.CREATE,
|
||||
parentFolderId,
|
||||
entity,
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const dispatchCreateFile = useCallback((parentFolderId, entity) => {
|
||||
entity.type = 'fileRef'
|
||||
dispatch({
|
||||
type: ACTION_TYPES.CREATE,
|
||||
parentFolderId,
|
||||
entity,
|
||||
})
|
||||
}, [])
|
||||
const dispatchCreateFile = useCallback(
|
||||
(parentFolderId: string, entity: any) => {
|
||||
entity.type = 'fileRef'
|
||||
dispatch({
|
||||
type: ACTION_TYPES.CREATE,
|
||||
parentFolderId,
|
||||
entity,
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const dispatchRename = useCallback(
|
||||
(id, newName) => {
|
||||
(id: string, newName: string) => {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.RENAME,
|
||||
newName,
|
||||
|
@ -204,11 +240,11 @@ export function FileTreeDataProvider({ children }) {
|
|||
[openDocId, setOpenDocName]
|
||||
)
|
||||
|
||||
const dispatchDelete = useCallback(id => {
|
||||
const dispatchDelete = useCallback((id: string) => {
|
||||
dispatch({ type: ACTION_TYPES.DELETE, id })
|
||||
}, [])
|
||||
|
||||
const dispatchMove = useCallback((entityId, toFolderId) => {
|
||||
const dispatchMove = useCallback((entityId: string, toFolderId: string) => {
|
||||
dispatch({ type: ACTION_TYPES.MOVE, entityId, toFolderId })
|
||||
}, [])
|
||||
|
||||
|
@ -247,7 +283,3 @@ export function FileTreeDataProvider({ children }) {
|
|||
</FileTreeDataContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
FileTreeDataProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
|
@ -12,6 +12,7 @@ import useScopeValue from '../hooks/use-scope-value'
|
|||
import useDetachLayout from '../hooks/use-detach-layout'
|
||||
import localStorage from '../../infrastructure/local-storage'
|
||||
import getMeta from '../../utils/meta'
|
||||
import { DetachRole } from './detach-context'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { BinaryFile } from '@/features/file-view/types/binary-file'
|
||||
import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
|
||||
|
@ -23,10 +24,10 @@ type LayoutContextValue = {
|
|||
reattach: () => void
|
||||
detach: () => void
|
||||
detachIsLinked: boolean
|
||||
detachRole: 'detacher' | 'detached' | null
|
||||
detachRole: DetachRole
|
||||
changeLayout: (newLayout: IdeLayout, newView?: IdeView) => void
|
||||
view: IdeView
|
||||
setView: (view: IdeView) => void
|
||||
view: IdeView | null
|
||||
setView: (view: IdeView | null) => void
|
||||
chatIsOpen: boolean
|
||||
setChatIsOpen: Dispatch<SetStateAction<LayoutContextValue['chatIsOpen']>>
|
||||
reviewPanelOpen: boolean
|
||||
|
@ -64,12 +65,12 @@ function setLayoutInLocalStorage(pdfLayout: IdeLayout) {
|
|||
|
||||
export const LayoutProvider: FC = ({ children }) => {
|
||||
// what to show in the "flat" view (editor or pdf)
|
||||
const [view, _setView] = useScopeValue<IdeView>('ui.view')
|
||||
const [view, _setView] = useScopeValue<IdeView | null>('ui.view')
|
||||
const [openFile] = useScopeValue<BinaryFile | null>('openFile')
|
||||
const historyToggleEmitter = useScopeEventEmitter('history:toggle', true)
|
||||
|
||||
const setView = useCallback(
|
||||
(value: IdeView) => {
|
||||
(value: IdeView | null) => {
|
||||
_setView(oldValue => {
|
||||
// ensure that the "history:toggle" event is broadcast when switching in or out of history view
|
||||
if (value === 'history' || oldValue === 'history') {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
FC,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
|
@ -7,7 +8,6 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import useScopeValue from '../hooks/use-scope-value'
|
||||
import useScopeValueSetterOnly from '../hooks/use-scope-value-setter-only'
|
||||
import usePersistedState from '../hooks/use-persisted-state'
|
||||
|
@ -36,57 +36,70 @@ import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-
|
|||
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
|
||||
import { useSplitTestContext } from '@/shared/context/split-test-context'
|
||||
|
||||
export const LocalCompileContext = createContext()
|
||||
type PdfFile = Record<string, any>
|
||||
|
||||
export const CompileContextPropTypes = {
|
||||
value: PropTypes.shape({
|
||||
autoCompile: PropTypes.bool.isRequired,
|
||||
clearingCache: PropTypes.bool.isRequired,
|
||||
clsiServerId: PropTypes.string,
|
||||
codeCheckFailed: PropTypes.bool.isRequired,
|
||||
compiling: PropTypes.bool.isRequired,
|
||||
deliveryLatencies: PropTypes.object.isRequired,
|
||||
draft: PropTypes.bool.isRequired,
|
||||
error: PropTypes.string,
|
||||
fileList: PropTypes.object,
|
||||
hasChanges: PropTypes.bool.isRequired,
|
||||
highlights: PropTypes.arrayOf(PropTypes.object),
|
||||
logEntries: PropTypes.object,
|
||||
logEntryAnnotations: PropTypes.object,
|
||||
pdfDownloadUrl: PropTypes.string,
|
||||
pdfFile: PropTypes.object,
|
||||
pdfUrl: PropTypes.string,
|
||||
pdfViewer: PropTypes.string,
|
||||
position: PropTypes.object,
|
||||
rawLog: PropTypes.string,
|
||||
setAutoCompile: PropTypes.func.isRequired,
|
||||
setDraft: PropTypes.func.isRequired,
|
||||
setError: PropTypes.func.isRequired,
|
||||
setHasLintingError: PropTypes.func.isRequired, // only for storybook
|
||||
setHighlights: PropTypes.func.isRequired,
|
||||
setPosition: PropTypes.func.isRequired,
|
||||
setShowCompileTimeWarning: PropTypes.func.isRequired,
|
||||
setShowLogs: PropTypes.func.isRequired,
|
||||
toggleLogs: PropTypes.func.isRequired,
|
||||
setStopOnFirstError: PropTypes.func.isRequired,
|
||||
setStopOnValidationError: PropTypes.func.isRequired,
|
||||
showCompileTimeWarning: PropTypes.bool.isRequired,
|
||||
showLogs: PropTypes.bool.isRequired,
|
||||
showNewCompileTimeoutUI: PropTypes.string,
|
||||
showFasterCompilesFeedbackUI: PropTypes.bool.isRequired,
|
||||
stopOnFirstError: PropTypes.bool.isRequired,
|
||||
stopOnValidationError: PropTypes.bool.isRequired,
|
||||
stoppedOnFirstError: PropTypes.bool.isRequired,
|
||||
uncompiled: PropTypes.bool,
|
||||
validationIssues: PropTypes.object,
|
||||
firstRenderDone: PropTypes.func.isRequired,
|
||||
cleanupCompileResult: PropTypes.func,
|
||||
}),
|
||||
export type CompileContext = {
|
||||
autoCompile: boolean
|
||||
clearingCache: boolean
|
||||
clsiServerId?: string
|
||||
codeCheckFailed: boolean
|
||||
compiling: boolean
|
||||
deliveryLatencies: Record<string, any>
|
||||
draft: boolean
|
||||
error?: string
|
||||
fileList?: Record<string, any>
|
||||
hasChanges: boolean
|
||||
highlights?: Record<string, any>[]
|
||||
isProjectOwner: boolean
|
||||
logEntries?: Record<string, any>
|
||||
logEntryAnnotations?: Record<string, any>
|
||||
pdfDownloadUrl?: string
|
||||
pdfFile?: PdfFile
|
||||
pdfUrl?: string
|
||||
pdfViewer?: string
|
||||
position?: Record<string, any>
|
||||
rawLog?: string
|
||||
setAutoCompile: (value: boolean) => void
|
||||
setDraft: (value: any) => void
|
||||
setError: (value: any) => void
|
||||
setHasLintingError: (value: any) => void // only for storybook
|
||||
setHighlights: (value: any) => void
|
||||
setPosition: (value: any) => void
|
||||
setShowCompileTimeWarning: (value: any) => void
|
||||
setShowLogs: (value: boolean) => void
|
||||
toggleLogs: () => void
|
||||
setStopOnFirstError: (value: boolean) => void
|
||||
setStopOnValidationError: (value: boolean) => void
|
||||
showCompileTimeWarning: boolean
|
||||
showLogs: boolean
|
||||
showNewCompileTimeoutUI?: string
|
||||
showFasterCompilesFeedbackUI: boolean
|
||||
stopOnFirstError: boolean
|
||||
stopOnValidationError: boolean
|
||||
stoppedOnFirstError: boolean
|
||||
uncompiled?: boolean
|
||||
validationIssues?: Record<string, any>
|
||||
firstRenderDone: () => void
|
||||
cleanupCompileResult?: () => void
|
||||
animateCompileDropdownArrow: boolean
|
||||
editedSinceCompileStarted: boolean
|
||||
lastCompileOptions: any
|
||||
setAnimateCompileDropdownArrow: (value: boolean) => void
|
||||
recompileFromScratch: () => void
|
||||
setCompiling: (value: boolean) => void
|
||||
startCompile: (options?: any) => void
|
||||
stopCompile: () => void
|
||||
setChangedAt: (value: any) => void
|
||||
setSavedAt: (value: any) => void
|
||||
clearCache: () => void
|
||||
syncToEntry: (value: any) => void
|
||||
}
|
||||
|
||||
LocalCompileContext.Provider.propTypes = CompileContextPropTypes
|
||||
export const LocalCompileContext = createContext<CompileContext | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export function LocalCompileProvider({ children }) {
|
||||
export const LocalCompileProvider: FC = ({ children }) => {
|
||||
const ide = useIdeContext()
|
||||
|
||||
const { hasPremiumCompile, isProjectOwner } = useEditorContext()
|
||||
|
@ -123,13 +136,14 @@ export function LocalCompileProvider({ children }) {
|
|||
const { pdfViewer, syntaxValidation } = userSettings
|
||||
|
||||
// the URL for downloading the PDF
|
||||
const [, setPdfDownloadUrl] = useScopeValueSetterOnly('pdf.downloadUrl')
|
||||
const [, setPdfDownloadUrl] =
|
||||
useScopeValueSetterOnly<string>('pdf.downloadUrl')
|
||||
|
||||
// the URL for loading the PDF in the preview pane
|
||||
const [, setPdfUrl] = useScopeValueSetterOnly('pdf.url')
|
||||
const [, setPdfUrl] = useScopeValueSetterOnly<string>('pdf.url')
|
||||
|
||||
// low level details for metrics
|
||||
const [pdfFile, setPdfFile] = useState()
|
||||
const [pdfFile, setPdfFile] = useState<PdfFile | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
setPdfDownloadUrl(pdfFile?.pdfDownloadUrl)
|
||||
|
@ -147,7 +161,7 @@ export function LocalCompileProvider({ children }) {
|
|||
const [clsiServerId, setClsiServerId] = useState()
|
||||
|
||||
// data received in response to a compile request
|
||||
const [data, setData] = useState()
|
||||
const [data, setData] = useState<Record<string, any>>()
|
||||
|
||||
// the rootDocId used in the most recent compile request, which may not be the
|
||||
// same as the project rootDocId. This is used to calculate correct paths when
|
||||
|
@ -187,13 +201,13 @@ export function LocalCompileProvider({ children }) {
|
|||
}, [setShowLogs])
|
||||
|
||||
// an error that occurred
|
||||
const [error, setError] = useState()
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
// the list of files that can be downloaded
|
||||
const [fileList, setFileList] = useState()
|
||||
const [fileList, setFileList] = useState<Record<string, any[]>>()
|
||||
|
||||
// the raw contents of the log file
|
||||
const [rawLog, setRawLog] = useState()
|
||||
const [rawLog, setRawLog] = useState<string>()
|
||||
|
||||
// validation issues from CLSI
|
||||
const [validationIssues, setValidationIssues] = useState()
|
||||
|
@ -246,7 +260,7 @@ export function LocalCompileProvider({ children }) {
|
|||
const { signal } = useAbortController()
|
||||
|
||||
const cleanupCompileResult = useCallback(() => {
|
||||
setPdfFile(null)
|
||||
setPdfFile(undefined)
|
||||
setLogEntries(null)
|
||||
setLogEntryAnnotations({})
|
||||
}, [setPdfFile, setLogEntries, setLogEntryAnnotations])
|
||||
|
@ -323,7 +337,8 @@ export function LocalCompileProvider({ children }) {
|
|||
}, [compiledOnce, currentDoc, compiler])
|
||||
|
||||
useEffect(() => {
|
||||
const compileTimeWarningEnabled = features?.compileTimeout <= 60
|
||||
const compileTimeWarningEnabled =
|
||||
features?.compileTimeout !== undefined && features.compileTimeout <= 60
|
||||
|
||||
if (compileTimeWarningEnabled && compiling && isProjectOwner) {
|
||||
const timeout = window.setTimeout(() => {
|
||||
|
@ -372,10 +387,10 @@ export function LocalCompileProvider({ children }) {
|
|||
// asynchronous (TODO: cancel on new compile?)
|
||||
setLogEntryAnnotations(null)
|
||||
setLogEntries(null)
|
||||
setRawLog(null)
|
||||
setRawLog(undefined)
|
||||
|
||||
handleLogFiles(outputFiles, data, abortController.signal).then(
|
||||
result => {
|
||||
(result: Record<string, any>) => {
|
||||
setRawLog(result.log)
|
||||
setLogEntries(result.logEntries)
|
||||
setLogEntryAnnotations(
|
||||
|
@ -702,17 +717,12 @@ export function LocalCompileProvider({ children }) {
|
|||
)
|
||||
}
|
||||
|
||||
LocalCompileProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
export function useLocalCompileContext(propTypes) {
|
||||
const data = useContext(LocalCompileContext)
|
||||
PropTypes.checkPropTypes(
|
||||
propTypes,
|
||||
data,
|
||||
'data',
|
||||
'LocalCompileContext.Provider'
|
||||
)
|
||||
return data
|
||||
export function useLocalCompileContext() {
|
||||
const context = useContext(LocalCompileContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useLocalCompileContext is only available inside LocalCompileProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
|
@ -1,54 +1,46 @@
|
|||
import { createContext, useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { FC, createContext, useContext, useMemo } from 'react'
|
||||
import useScopeValue from '../hooks/use-scope-value'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { UserId } from '../../../../types/user'
|
||||
import { PublicAccessLevel } from '../../../../types/public-access-level'
|
||||
import * as ReviewPanel from '@/features/ide-react/context/review-panel/types/review-panel-state'
|
||||
|
||||
const ProjectContext = createContext()
|
||||
const ProjectContext = createContext<
|
||||
| {
|
||||
_id: string
|
||||
name: string
|
||||
rootDocId?: string
|
||||
members: { _id: UserId; email: string; privileges: string }[]
|
||||
invites: { _id: UserId }[]
|
||||
features: {
|
||||
collaborators?: number
|
||||
compileGroup?: 'alpha' | 'standard' | 'priority'
|
||||
trackChanges?: boolean
|
||||
trackChangesVisible?: boolean
|
||||
references?: boolean
|
||||
mendeley?: boolean
|
||||
zotero?: boolean
|
||||
versioning?: boolean
|
||||
gitBridge?: boolean
|
||||
referencesSearch?: boolean
|
||||
}
|
||||
publicAccessLevel?: PublicAccessLevel
|
||||
owner: {
|
||||
_id: UserId
|
||||
email: string
|
||||
}
|
||||
showNewCompileTimeoutUI?: string
|
||||
tags: {
|
||||
_id: string
|
||||
name: string
|
||||
color?: string
|
||||
}[]
|
||||
trackChangesState: ReviewPanel.Value<'trackChangesState'>
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
export const projectShape = {
|
||||
_id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
rootDocId: PropTypes.string,
|
||||
members: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
})
|
||||
),
|
||||
invites: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
})
|
||||
),
|
||||
features: PropTypes.shape({
|
||||
collaborators: PropTypes.number,
|
||||
compileGroup: PropTypes.oneOf(['alpha', 'standard', 'priority']),
|
||||
trackChangesVisible: PropTypes.bool,
|
||||
references: PropTypes.bool,
|
||||
mendeley: PropTypes.bool,
|
||||
zotero: PropTypes.bool,
|
||||
versioning: PropTypes.bool,
|
||||
gitBridge: PropTypes.bool,
|
||||
}),
|
||||
publicAccessLevel: PropTypes.string,
|
||||
owner: PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
}),
|
||||
useNewCompileTimeoutUI: PropTypes.string,
|
||||
tags: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
color: PropTypes.string,
|
||||
})
|
||||
).isRequired,
|
||||
}
|
||||
|
||||
ProjectContext.Provider.propTypes = {
|
||||
value: PropTypes.shape(projectShape),
|
||||
}
|
||||
|
||||
export function useProjectContext(propTypes) {
|
||||
export function useProjectContext() {
|
||||
const context = useContext(ProjectContext)
|
||||
|
||||
if (!context) {
|
||||
|
@ -57,13 +49,6 @@ export function useProjectContext(propTypes) {
|
|||
)
|
||||
}
|
||||
|
||||
PropTypes.checkPropTypes(
|
||||
propTypes,
|
||||
context,
|
||||
'data',
|
||||
'ProjectContext.Provider'
|
||||
)
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
|
@ -76,7 +61,7 @@ const projectFallback = {
|
|||
features: {},
|
||||
}
|
||||
|
||||
export function ProjectProvider({ children }) {
|
||||
export const ProjectProvider: FC = ({ children }) => {
|
||||
const [project] = useScopeValue('project', true)
|
||||
|
||||
const {
|
||||
|
@ -96,7 +81,7 @@ export function ProjectProvider({ children }) {
|
|||
() =>
|
||||
getMeta('ol-projectTags', [])
|
||||
// `tag.name` data may be null for some old users
|
||||
.map(tag => ({ ...tag, name: tag.name ?? '' })),
|
||||
.map((tag: any) => ({ ...tag, name: tag.name ?? '' })),
|
||||
[]
|
||||
)
|
||||
|
||||
|
@ -145,7 +130,3 @@ export function ProjectProvider({ children }) {
|
|||
<ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
ProjectProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import { FC } from 'react'
|
||||
import createSharedContext from 'react2angular-shared-context'
|
||||
|
||||
import { UserProvider } from './user-context'
|
||||
import { IdeAngularProvider } from './ide-angular-provider'
|
||||
import { EditorProvider } from './editor-context'
|
||||
|
@ -16,8 +15,9 @@ import { ProjectSettingsProvider } from '@/features/editor-left-menu/context/pro
|
|||
import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path'
|
||||
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
|
||||
import { OutlineProvider } from '@/features/ide-react/context/outline-context'
|
||||
import { Ide } from '@/shared/context/ide-context'
|
||||
|
||||
export function ContextRoot({ children, ide }) {
|
||||
export const ContextRoot: FC<{ ide?: Ide }> = ({ children, ide }) => {
|
||||
return (
|
||||
<SplitTestProvider>
|
||||
<IdeAngularProvider ide={ide}>
|
||||
|
@ -51,9 +51,4 @@ export function ContextRoot({ children, ide }) {
|
|||
)
|
||||
}
|
||||
|
||||
ContextRoot.propTypes = {
|
||||
children: PropTypes.any,
|
||||
ide: PropTypes.object,
|
||||
}
|
||||
|
||||
export const rootContext = createSharedContext(ContextRoot)
|
|
@ -1,45 +0,0 @@
|
|||
import { createContext, useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import getMeta from '../../utils/meta'
|
||||
|
||||
export const SplitTestContext = createContext()
|
||||
|
||||
SplitTestContext.Provider.propTypes = {
|
||||
value: PropTypes.shape({
|
||||
splitTestVariants: PropTypes.object.isRequired,
|
||||
splitTestInfo: PropTypes.object.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export function SplitTestProvider({ children }) {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
splitTestVariants: getMeta('ol-splitTestVariants') || {},
|
||||
splitTestInfo: getMeta('ol-splitTestInfo') || {},
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<SplitTestContext.Provider value={value}>
|
||||
{children}
|
||||
</SplitTestContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
SplitTestProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
export function useSplitTestContext(propTypes) {
|
||||
const context = useContext(SplitTestContext)
|
||||
|
||||
PropTypes.checkPropTypes(
|
||||
propTypes,
|
||||
context,
|
||||
'data',
|
||||
'SplitTestContext.Provider'
|
||||
)
|
||||
|
||||
return context
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { createContext, FC, useContext, useMemo } from 'react'
|
||||
import getMeta from '../../utils/meta'
|
||||
|
||||
type SplitTestVariants = Record<string, any>
|
||||
type SplitTestInfo = Record<string, any>
|
||||
|
||||
export const SplitTestContext = createContext<
|
||||
| {
|
||||
splitTestVariants: SplitTestVariants
|
||||
splitTestInfo: SplitTestInfo
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
export const SplitTestProvider: FC = ({ children }) => {
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
splitTestVariants: getMeta('ol-splitTestVariants') || {},
|
||||
splitTestInfo: getMeta('ol-splitTestInfo') || {},
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<SplitTestContext.Provider value={value}>
|
||||
{children}
|
||||
</SplitTestContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useSplitTestContext() {
|
||||
const context = useContext(SplitTestContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useSplitTestContext is only available within SplitTestProvider'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
import { createContext, useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import getMeta from '../../utils/meta'
|
||||
|
||||
export const UserContext = createContext()
|
||||
|
||||
UserContext.Provider.propTypes = {
|
||||
value: PropTypes.shape({
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
isAdmin: PropTypes.boolean,
|
||||
email: PropTypes.string,
|
||||
allowedFreeTrial: PropTypes.boolean,
|
||||
first_name: PropTypes.string,
|
||||
last_name: PropTypes.string,
|
||||
alphaProgram: PropTypes.boolean,
|
||||
betaProgram: PropTypes.boolean,
|
||||
labsProgram: PropTypes.boolean,
|
||||
signUpDate: PropTypes.string,
|
||||
features: PropTypes.shape({
|
||||
dropbox: PropTypes.boolean,
|
||||
github: PropTypes.boolean,
|
||||
mendeley: PropTypes.boolean,
|
||||
zotero: PropTypes.boolean,
|
||||
references: PropTypes.boolean,
|
||||
compileTimeout: PropTypes.number,
|
||||
gitBridge: PropTypes.boolean,
|
||||
}),
|
||||
refProviders: PropTypes.shape({
|
||||
mendeley: PropTypes.boolean,
|
||||
zotero: PropTypes.boolean,
|
||||
}),
|
||||
writefull: PropTypes.shape({
|
||||
enabled: PropTypes.boolean,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
|
||||
export function UserProvider({ children }) {
|
||||
const user = getMeta('ol-user')
|
||||
|
||||
return <UserContext.Provider value={user}>{children}</UserContext.Provider>
|
||||
}
|
||||
|
||||
UserProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
export function useUserContext(propTypes) {
|
||||
const data = useContext(UserContext)
|
||||
if (!data) {
|
||||
throw new Error(
|
||||
'useUserContext is only available inside UserContext, or `ol-user` meta is not defined'
|
||||
)
|
||||
}
|
||||
|
||||
PropTypes.checkPropTypes(propTypes, data, 'data', 'UserContext.Provider')
|
||||
return data
|
||||
}
|
23
services/web/frontend/js/shared/context/user-context.tsx
Normal file
23
services/web/frontend/js/shared/context/user-context.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { createContext, FC, useContext } from 'react'
|
||||
import getMeta from '../../utils/meta'
|
||||
import { User } from '../../../../types/user'
|
||||
|
||||
export const UserContext = createContext<User | undefined>(undefined)
|
||||
|
||||
export const UserProvider: FC = ({ children }) => {
|
||||
const user = getMeta('ol-user')
|
||||
|
||||
return <UserContext.Provider value={user}>{children}</UserContext.Provider>
|
||||
}
|
||||
|
||||
export function useUserContext() {
|
||||
const context = useContext(UserContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useUserContext is only available inside UserContext, or `ol-user` meta is not defined'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
import { useCallback, useRef } from 'react'
|
||||
|
||||
export default function useCallbackHandlers() {
|
||||
const handlersRef = useRef(new Set<(...arg: unknown[]) => void>())
|
||||
const handlersRef = useRef(new Set<(...arg: any[]) => void>())
|
||||
|
||||
const addHandler = useCallback((handler: (...args: unknown[]) => void) => {
|
||||
const addHandler = useCallback((handler: (...args: any[]) => void) => {
|
||||
handlersRef.current.add(handler)
|
||||
}, [])
|
||||
|
||||
const deleteHandler = useCallback((handler: (...args: unknown[]) => void) => {
|
||||
const deleteHandler = useCallback((handler: (...args: any[]) => void) => {
|
||||
handlersRef.current.delete(handler)
|
||||
}, [])
|
||||
|
||||
const callHandlers = useCallback((...args: unknown[]) => {
|
||||
const callHandlers = useCallback((...args: any[]) => {
|
||||
for (const handler of handlersRef.current) {
|
||||
handler(...args)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useEffect } from 'react'
|
|||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
import { mockScope } from './scope'
|
||||
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
|
||||
import { FindResult } from '@/features/file-tree/util/path'
|
||||
|
||||
const mockHighlights = [
|
||||
{
|
||||
|
@ -36,11 +37,7 @@ const mockPosition: Position = {
|
|||
pageSize: { height: 500, width: 500 },
|
||||
}
|
||||
|
||||
type Entity = {
|
||||
type: string
|
||||
}
|
||||
|
||||
const mockSelectedEntities: Entity[] = [{ type: 'doc' }]
|
||||
const mockSelectedEntities = [{ type: 'doc' }] as FindResult[]
|
||||
|
||||
const WithPosition = ({ mockPosition }: { mockPosition: Position }) => {
|
||||
const { setPosition } = useCompileContext()
|
||||
|
@ -65,7 +62,7 @@ const setDetachedPosition = (mockPosition: Position) => {
|
|||
const WithSelectedEntities = ({
|
||||
mockSelectedEntities = [],
|
||||
}: {
|
||||
mockSelectedEntities: Entity[]
|
||||
mockSelectedEntities: FindResult[]
|
||||
}) => {
|
||||
const { setSelectedEntities } = useFileTreeData()
|
||||
|
||||
|
@ -153,7 +150,9 @@ describe('<PdfSynctexControls/>', function () {
|
|||
<EditorProviders scope={scope}>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities
|
||||
mockSelectedEntities={[{ type: 'doc' }, { type: 'doc' }]}
|
||||
mockSelectedEntities={
|
||||
[{ type: 'doc' }, { type: 'doc' }] as FindResult[]
|
||||
}
|
||||
/>
|
||||
<PdfSynctexControls />
|
||||
</EditorProviders>
|
||||
|
@ -174,7 +173,9 @@ describe('<PdfSynctexControls/>', function () {
|
|||
cy.mount(
|
||||
<EditorProviders scope={scope}>
|
||||
<WithPosition mockPosition={mockPosition} />
|
||||
<WithSelectedEntities mockSelectedEntities={[{ type: 'file' }]} />
|
||||
<WithSelectedEntities
|
||||
mockSelectedEntities={[{ type: 'fileRef' }] as FindResult[]}
|
||||
/>
|
||||
<PdfSynctexControls />
|
||||
</EditorProviders>
|
||||
)
|
||||
|
|
|
@ -6,7 +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'
|
||||
import { User, UserId } from '../../../../../types/user'
|
||||
|
||||
const Container: FC = ({ children }) => (
|
||||
<div style={{ width: 785, height: 785 }}>{children}</div>
|
||||
|
@ -863,7 +863,7 @@ describe('autocomplete', { scrollBehavior: false }, function () {
|
|||
const user = {
|
||||
id: '123abd' as UserId,
|
||||
email: 'testuser@example.com',
|
||||
}
|
||||
} as User
|
||||
cy.mount(
|
||||
<Container>
|
||||
<EditorProviders user={user} scope={scope}>
|
||||
|
|
|
@ -38,7 +38,7 @@ const memberGroupSubscriptions: MemberGroupSubscription[] = [
|
|||
email: 'someone@example.com',
|
||||
},
|
||||
},
|
||||
]
|
||||
] as MemberGroupSubscription[]
|
||||
|
||||
describe('<GroupSubscriptionMemberships />', function () {
|
||||
beforeEach(function () {
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
// todo: maybe change this to just tutorials, and move it from editor context to user context?
|
||||
export type EditorTutorials = {
|
||||
inactiveTutorials: [string]
|
||||
deactivateTutorial: (key: string) => void
|
||||
currentPopup: string
|
||||
setCurrentPopup: (id: string) => void
|
||||
}
|
|
@ -9,8 +9,14 @@ export type UserId = Brand<string, 'UserId'>
|
|||
|
||||
export type User = {
|
||||
id: UserId
|
||||
isAdmin?: boolean
|
||||
email: string
|
||||
allowedFreeTrial?: boolean
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
alphaProgram?: boolean
|
||||
betaProgram?: boolean
|
||||
labsProgram?: boolean
|
||||
signUpDate?: string // date string
|
||||
features?: {
|
||||
collaborators?: number
|
||||
|
@ -29,6 +35,9 @@ export type User = {
|
|||
zotero?: boolean
|
||||
}
|
||||
refProviders?: RefProviders
|
||||
writefull?: {
|
||||
enabled: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type MongoUser = Pick<User, Exclude<keyof User, 'id'>> & { _id: string }
|
||||
|
|
|
@ -28,6 +28,10 @@ declare global {
|
|||
logEntryAnnotations: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
socket: {
|
||||
on: (event: string, listener: any) => void
|
||||
removeListener: (event: string, listener: any) => void
|
||||
}
|
||||
}
|
||||
isRestrictedTokenMember: boolean
|
||||
_reportCM6Perf: () => void
|
||||
|
@ -42,6 +46,8 @@ declare global {
|
|||
enterprise?: boolean
|
||||
useRecaptchaNet?: boolean
|
||||
}
|
||||
brandVariation?: Record<string, any>
|
||||
data?: Record<string, any>
|
||||
expectingLinkedFileRefreshedSocketFor?: string | null
|
||||
writefull?: any
|
||||
io?: any
|
||||
|
|
Loading…
Reference in a new issue