Merge pull request #15684 from overleaf/td-ide-page-main-doc-left-menu

Add docs to FileTreeDataContext to replace 'docs' scope value in React code

GitOrigin-RevId: 430f795eb0cd17f0f4fab9c61e46fb04ff3030b3
This commit is contained in:
Tim Down 2023-11-13 11:11:33 +00:00 committed by Copybot
parent 1c820de200
commit 6f34a84ebd
14 changed files with 90 additions and 60 deletions

View file

@ -2,16 +2,16 @@ import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import isValidTeXFile from '../../../../main/is-valid-tex-file' import isValidTeXFile from '../../../../main/is-valid-tex-file'
import { useEditorContext } from '../../../../shared/context/editor-context' import { useEditorContext } from '../../../../shared/context/editor-context'
import useScopeValue from '../../../../shared/hooks/use-scope-value'
import { useProjectSettingsContext } from '../../context/project-settings-context' import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select' import SettingsMenuSelect from './settings-menu-select'
import type { Option } from './settings-menu-select' import type { Option } from './settings-menu-select'
import type { MainDocument } from '../../../../../../types/project-settings' import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { MainDocument } from '../../../../../../types/project-settings'
export default function SettingsDocument() { export default function SettingsDocument() {
const { t } = useTranslation() const { t } = useTranslation()
const { permissionsLevel } = useEditorContext() const { permissionsLevel } = useEditorContext()
const [docs] = useScopeValue<MainDocument[] | undefined>('docs') const docs: MainDocument[] = useFileTreeData().docs
const { rootDocId, setRootDocId } = useProjectSettingsContext() const { rootDocId, setRootDocId } = useProjectSettingsContext()
const validDocsOptions = useMemo(() => { const validDocsOptions = useMemo(() => {

View file

@ -0,0 +1,31 @@
import { Folder } from '../../../../../types/folder'
import { DocId, MainDocument } from '../../../../../types/project-settings'
function findAllDocsInFolder(folder: Folder, path = '') {
const docs = folder.docs.map<MainDocument>(doc => ({
doc: { id: doc._id as DocId, name: doc.name },
path: path + doc.name,
}))
for (const subFolder of folder.folders) {
docs.push(...findAllDocsInFolder(subFolder, `${path}${subFolder.name}/`))
}
return docs
}
export function docsInFolder(folder: Folder) {
const docsInTree = findAllDocsInFolder(folder)
docsInTree.sort(function (a, b) {
const aDepth = (a.path.match(/\//g) || []).length
const bDepth = (b.path.match(/\//g) || []).length
if (aDepth - bDepth !== 0) {
return -(aDepth - bDepth) // Deeper path == folder first
} else if (a.path < b.path) {
return -1
} else if (a.path > b.path) {
return 1
} else {
return 0
}
})
return docsInTree
}

View file

@ -56,10 +56,6 @@ function populatePdfScope(store: ReactScopeValueStore) {
store.allowNonExistentPath('pdf', true) store.allowNonExistentPath('pdf', true)
} }
function populateFileTreeScope(store: ReactScopeValueStore) {
store.set('docs', [])
}
function createReactScopeValueStore(projectId: string) { function createReactScopeValueStore(projectId: string) {
const scopeStore = new ReactScopeValueStore() const scopeStore = new ReactScopeValueStore()
@ -76,7 +72,6 @@ function createReactScopeValueStore(projectId: string) {
populateSettingsScope(scopeStore) populateSettingsScope(scopeStore)
populateOnlineUsersScope(scopeStore) populateOnlineUsersScope(scopeStore)
populateReferenceScope(scopeStore) populateReferenceScope(scopeStore)
populateFileTreeScope(scopeStore)
populateReviewPanelScope(scopeStore) populateReviewPanelScope(scopeStore)
scopeStore.allowNonExistentPath('hasLintingError') scopeStore.allowNonExistentPath('hasLintingError')

View file

@ -33,7 +33,6 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
'reviewPanel.commentThreads', 'reviewPanel.commentThreads',
true true
) )
const [docs] = useScopeValue<ReviewPanel.Value<'docs'>>('docs')
const [entries] = useScopeValue<ReviewPanel.Value<'entries'>>( const [entries] = useScopeValue<ReviewPanel.Value<'entries'>>(
'reviewPanel.entries', 'reviewPanel.entries',
true true
@ -156,7 +155,6 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
() => ({ () => ({
collapsed, collapsed,
commentThreads, commentThreads,
docs,
entries, entries,
entryHover, entryHover,
isAddingComment, isAddingComment,
@ -183,7 +181,6 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
[ [
collapsed, collapsed,
commentThreads, commentThreads,
docs,
entries, entries,
entryHover, entryHover,
isAddingComment, isAddingComment,

View file

@ -6,7 +6,6 @@ export default function populateReviewPanelScope(store: ReactScopeValueStore) {
store.set('reviewPanel.overview.loading', false) store.set('reviewPanel.overview.loading', false)
store.set('reviewPanel.nVisibleSelectedChanges', 0) store.set('reviewPanel.nVisibleSelectedChanges', 0)
store.set('reviewPanel.commentThreads', {}) store.set('reviewPanel.commentThreads', {})
store.set('docs', undefined)
store.set('reviewPanel.entries', {}) store.set('reviewPanel.entries', {})
store.set('loadingThreads', true) store.set('loadingThreads', true)
store.set('permissions', { store.set('permissions', {

View file

@ -5,9 +5,12 @@ import Nav from './nav'
import Icon from '../../../../shared/components/icon' import Icon from '../../../../shared/components/icon'
import OverviewFile from './overview-file' import OverviewFile from './overview-file'
import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context' import { useReviewPanelValueContext } from '../../context/review-panel/review-panel-context'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { MainDocument } from '../../../../../../types/project-settings'
function OverviewContainer() { function OverviewContainer() {
const { loading, docs } = useReviewPanelValueContext() const { loading } = useReviewPanelValueContext()
const docs: MainDocument[] = useFileTreeData().docs
return ( return (
<Container> <Container>

View file

@ -13,8 +13,12 @@ import {
ThreadId, ThreadId,
} from '../../../../../../../types/review-panel/review-panel' } from '../../../../../../../types/review-panel/review-panel'
import { ReviewPanelResolvedCommentThread } from '../../../../../../../types/review-panel/comment-thread' import { ReviewPanelResolvedCommentThread } from '../../../../../../../types/review-panel/comment-thread'
import { DocId } from '../../../../../../../types/project-settings' import {
DocId,
MainDocument,
} from '../../../../../../../types/project-settings'
import { ReviewPanelEntry } from '../../../../../../../types/review-panel/entry' import { ReviewPanelEntry } from '../../../../../../../types/review-panel/entry'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
export interface FilteredResolvedComments export interface FilteredResolvedComments
extends ReviewPanelResolvedCommentThread { extends ReviewPanelResolvedCommentThread {
@ -29,8 +33,9 @@ function ResolvedCommentsDropdown() {
const { t } = useTranslation() const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const { docs, commentThreads, resolvedComments, permissions } = const { commentThreads, resolvedComments, permissions } =
useReviewPanelValueContext() useReviewPanelValueContext()
const docs: MainDocument[] = useFileTreeData().docs
const { refreshResolvedCommentsDropdown } = useReviewPanelUpdaterFnsContext() const { refreshResolvedCommentsDropdown } = useReviewPanelUpdaterFnsContext()

View file

@ -26,7 +26,6 @@ function useAngularReviewPanelState(): ReviewPanelState {
'reviewPanel.commentThreads', 'reviewPanel.commentThreads',
true true
) )
const [docs] = useScopeValue<ReviewPanel.Value<'docs'>>('docs')
const [entries] = useScopeValue<ReviewPanel.Value<'entries'>>( const [entries] = useScopeValue<ReviewPanel.Value<'entries'>>(
'reviewPanel.entries', 'reviewPanel.entries',
true true
@ -143,7 +142,6 @@ function useAngularReviewPanelState(): ReviewPanelState {
() => ({ () => ({
collapsed, collapsed,
commentThreads, commentThreads,
docs,
entries, entries,
entryHover, entryHover,
isAddingComment, isAddingComment,
@ -170,7 +168,6 @@ function useAngularReviewPanelState(): ReviewPanelState {
[ [
collapsed, collapsed,
commentThreads, commentThreads,
docs,
entries, entries,
entryHover, entryHover,
isAddingComment, isAddingComment,

View file

@ -7,10 +7,7 @@ import {
SubView, SubView,
ThreadId, ThreadId,
} from '../../../../../../../types/review-panel/review-panel' } from '../../../../../../../types/review-panel/review-panel'
import { import { DocId } from '../../../../../../../types/project-settings'
DocId,
MainDocument,
} from '../../../../../../../types/project-settings'
import { dispatchReviewPanelLayout } from '../../../extensions/changes/change-manager' import { dispatchReviewPanelLayout } from '../../../extensions/changes/change-manager'
/* eslint-disable no-use-before-define */ /* eslint-disable no-use-before-define */
@ -18,7 +15,6 @@ export interface ReviewPanelState {
values: { values: {
collapsed: Record<DocId, boolean> collapsed: Record<DocId, boolean>
commentThreads: ReviewPanelCommentThreads commentThreads: ReviewPanelCommentThreads
docs: MainDocument[] | undefined
entries: ReviewPanelEntries entries: ReviewPanelEntries
entryHover: boolean entryHover: boolean
isAddingComment: boolean isAddingComment: boolean

View file

@ -16,6 +16,7 @@ import {
} from '../../features/file-tree/util/mutate-in-tree' } from '../../features/file-tree/util/mutate-in-tree'
import { countFiles } from '../../features/file-tree/util/count-in-tree' import { countFiles } from '../../features/file-tree/util/count-in-tree'
import useDeepCompareEffect from '../../shared/hooks/use-deep-compare-effect' import useDeepCompareEffect from '../../shared/hooks/use-deep-compare-effect'
import { docsInFolder } from '@/features/file-tree/util/docs-in-folder'
const FileTreeDataContext = createContext() const FileTreeDataContext = createContext()
@ -147,6 +148,11 @@ export function FileTreeDataProvider({ children }) {
const [selectedEntities, setSelectedEntities] = useState([]) const [selectedEntities, setSelectedEntities] = useState([])
const docs = useMemo(
() => (fileTreeData ? docsInFolder(fileTreeData) : undefined),
[fileTreeData]
)
useDeepCompareEffect(() => { useDeepCompareEffect(() => {
dispatch({ dispatch({
type: ACTION_TYPES.RESET, type: ACTION_TYPES.RESET,
@ -210,6 +216,7 @@ export function FileTreeDataProvider({ children }) {
hasFolders: fileTreeData?.folders.length > 0, hasFolders: fileTreeData?.folders.length > 0,
selectedEntities, selectedEntities,
setSelectedEntities, setSelectedEntities,
docs,
} }
}, [ }, [
dispatchCreateDoc, dispatchCreateDoc,
@ -222,6 +229,7 @@ export function FileTreeDataProvider({ children }) {
fileTreeData, fileTreeData,
selectedEntities, selectedEntities,
setSelectedEntities, setSelectedEntities,
docs,
]) ])
return ( return (

View file

@ -1,12 +1,13 @@
import EditorLeftMenu from '../../../../frontend/js/features/editor-left-menu/components/editor-left-menu' import EditorLeftMenu from '../../../../frontend/js/features/editor-left-menu/components/editor-left-menu'
import { import {
AllowedImageName, AllowedImageName,
MainDocument,
OverallThemeMeta, OverallThemeMeta,
SpellCheckLanguage, SpellCheckLanguage,
} from '../../../../types/project-settings' } from '../../../../types/project-settings'
import { EditorProviders } from '../../helpers/editor-providers' import { EditorProviders } from '../../helpers/editor-providers'
import { mockScope } from './scope' import { mockScope } from './scope'
import { Folder } from '../../../../types/folder'
import { docsInFolder } from '@/features/file-tree/util/docs-in-folder'
describe('<EditorLeftMenu />', function () { describe('<EditorLeftMenu />', function () {
beforeEach(function () { beforeEach(function () {
@ -390,40 +391,37 @@ describe('<EditorLeftMenu />', function () {
}) })
it('shows document menu correctly', function () { it('shows document menu correctly', function () {
const docs: MainDocument[] = [ const rootFolder: Folder = {
{ _id: 'root-folder-id',
path: 'main.tex', name: 'rootFolder',
doc: { docs: [
{
_id: 'id1',
name: 'main.tex', name: 'main.tex',
id: 'id1', },
type: 'doc', {
selected: false, _id: 'id2',
} as MainDocument['doc'],
},
{
path: 'folder/main2.tex',
doc: {
name: 'main2.tex', name: 'main2.tex',
id: 'id2', },
type: 'doc', ],
selected: false, fileRefs: [],
} as MainDocument['doc'], folders: [],
}, }
]
const scope = mockScope({ const scope = mockScope({
ui: { ui: {
leftMenuShown: true, leftMenuShown: true,
}, },
docs,
}) })
cy.mount( cy.mount(
<EditorProviders scope={scope}> <EditorProviders scope={scope} rootFolder={[rootFolder as any]}>
<EditorLeftMenu /> <EditorLeftMenu />
</EditorProviders> </EditorProviders>
) )
const docs = docsInFolder(rootFolder)
cy.get<HTMLOptionElement>('#settings-menu-rootDocId option').then( cy.get<HTMLOptionElement>('#settings-menu-rootDocId option').then(
options => { options => {
const values = [...options].map(o => o.value) const values = [...options].map(o => o.value)

View file

@ -5,21 +5,23 @@ import fetchMock from 'fetch-mock'
import SettingsDocument from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-document' import SettingsDocument from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-document'
import * as isValidTeXFileModule from '../../../../../../frontend/js/main/is-valid-tex-file' import * as isValidTeXFileModule from '../../../../../../frontend/js/main/is-valid-tex-file'
import { renderWithEditorContext } from '../../../../helpers/render-with-context' import { renderWithEditorContext } from '../../../../helpers/render-with-context'
import type { MainDocument } from '../../../../../../types/project-settings' import { Folder } from '../../../../../../types/folder'
describe('<SettingsDocument />', function () { describe('<SettingsDocument />', function () {
let isValidTeXFileStub: sinon.SinonStub let isValidTeXFileStub: sinon.SinonStub
const docs: MainDocument[] = [
{ const rootFolder: Folder = {
path: 'main.tex', _id: 'root-folder-id',
doc: { name: 'rootFolder',
docs: [
{
_id: '123abc',
name: 'main.tex', name: 'main.tex',
id: '123abc', },
type: 'doc', ],
selected: false, fileRefs: [],
} as MainDocument['doc'], folders: [],
}, }
]
beforeEach(function () { beforeEach(function () {
isValidTeXFileStub = sinon isValidTeXFileStub = sinon
@ -34,9 +36,7 @@ describe('<SettingsDocument />', function () {
it('shows correct menu', async function () { it('shows correct menu', async function () {
renderWithEditorContext(<SettingsDocument />, { renderWithEditorContext(<SettingsDocument />, {
scope: { rootFolder: [rootFolder],
docs,
},
}) })
const select = screen.getByLabelText('Main document') const select = screen.getByLabelText('Main document')

View file

@ -22,6 +22,9 @@ describe('<ReviewPanel />', function () {
const scope = mockScope('') const scope = mockScope('')
scope.editor.showVisual = true scope.editor.showVisual = true
// The tests expect no documents, so remove them from the scope
scope.project.rootFolder = []
cy.wrap(scope).as('scope') cy.wrap(scope).as('scope')
cy.mount( cy.mount(

View file

@ -11,8 +11,6 @@ export type MainDocument = {
doc: { doc: {
name: string name: string
id: DocId id: DocId
type: string
selected: boolean
} }
path: string path: string
} }