Merge pull request #3550 from overleaf/ta-disconected-overlay

Add Disconnected Overlay Over File Tree

GitOrigin-RevId: bdcd4f58effe841eb223abbb852e0b0f574efefd
This commit is contained in:
Eric Mc Sween 2021-01-14 08:57:33 -05:00 committed by Copybot
parent e1ffeef06a
commit dc48ba1d61
10 changed files with 83 additions and 4 deletions

View file

@ -18,6 +18,7 @@ aside.editor-sidebar.full-size(
has-write-permissions="hasWritePermissions" has-write-permissions="hasWritePermissions"
on-select="onSelect" on-select="onSelect"
on-init="onInit" on-init="onInit"
is-connected="isConnected"
) )
div(ng-controller="FileTreeController") div(ng-controller="FileTreeController")

View file

@ -24,7 +24,8 @@ function FileTreeRoot({
rootDocId, rootDocId,
hasWritePermissions, hasWritePermissions,
onSelect, onSelect,
onInit onInit,
isConnected
}) { }) {
const isReady = projectId && rootFolder const isReady = projectId && rootFolder
@ -41,6 +42,7 @@ function FileTreeRoot({
rootDocId={rootDocId} rootDocId={rootDocId}
onSelect={onSelect} onSelect={onSelect}
> >
{isConnected ? null : <div className="disconnected-overlay" />}
<FileTreeToolbar /> <FileTreeToolbar />
<FileTreeContextMenu /> <FileTreeContextMenu />
<div className="file-tree-inner"> <div className="file-tree-inner">
@ -83,7 +85,8 @@ FileTreeRoot.propTypes = {
rootDocId: PropTypes.string, rootDocId: PropTypes.string,
hasWritePermissions: PropTypes.bool.isRequired, hasWritePermissions: PropTypes.bool.isRequired,
onSelect: PropTypes.func.isRequired, onSelect: PropTypes.func.isRequired,
onInit: PropTypes.func.isRequired onInit: PropTypes.func.isRequired,
isConnected: PropTypes.bool.isRequired
} }
export default withErrorBoundary(FileTreeRoot, FileTreeError) export default withErrorBoundary(FileTreeRoot, FileTreeError)

View file

@ -13,6 +13,7 @@ App.controller('ReactFileTreeController', function(
$scope.rootFolder = null $scope.rootFolder = null
$scope.rootDocId = null $scope.rootDocId = null
$scope.hasWritePermissions = false $scope.hasWritePermissions = false
$scope.isConnected = true
$scope.$on('project:joined', () => { $scope.$on('project:joined', () => {
$scope.rootFolder = $scope.project.rootFolder $scope.rootFolder = $scope.project.rootFolder
@ -30,6 +31,23 @@ App.controller('ReactFileTreeController', function(
) )
}) })
// Set isConnected to true if:
// - connection state is 'ready', OR
// - connection state is 'waitingCountdown' and reconnection_countdown is null
// The added complexity is needed because in Firefox on page reload the
// connection state goes into 'waitingCountdown' before being hidden and we
// don't want to show a disconnect UI.
function updateIsConnected() {
const isReady = $scope.connection.state === 'ready'
const willStartCountdown =
$scope.connection.state === 'waitingCountdown' &&
$scope.connection.reconnection_countdown === null
$scope.isConnected = isReady || willStartCountdown
}
$scope.$watch('connection.state', updateIsConnected)
$scope.$watch('connection.reconnection_countdown', updateIsConnected)
$scope.onInit = () => { $scope.onInit = () => {
// HACK: resize the vertical pane on init after a 0ms timeout. We do not // HACK: resize the vertical pane on init after a 0ms timeout. We do not
// understand why this is necessary but without this the resized handle is // understand why this is necessary but without this the resized handle is

View file

@ -86,6 +86,9 @@ FullTree.parameters = { setupMocks: defaultSetupMocks }
export const ReadOnly = args => <FileTreeRoot {...args} /> export const ReadOnly = args => <FileTreeRoot {...args} />
ReadOnly.args = { hasWritePermissions: false } ReadOnly.args = { hasWritePermissions: false }
export const Disconnected = args => <FileTreeRoot {...args} />
Disconnected.args = { isConnected: false }
export const NetworkErrors = args => <FileTreeRoot {...args} /> export const NetworkErrors = args => <FileTreeRoot {...args} />
NetworkErrors.parameters = { NetworkErrors.parameters = {
setupMocks: () => { setupMocks: () => {
@ -116,7 +119,8 @@ export default {
rootFolder: rootFolderBase, rootFolder: rootFolderBase,
hasWritePermissions: true, hasWritePermissions: true,
projectId: '123abc', projectId: '123abc',
rootDocId: '5e74f1a7ce17ae0041dfd056' rootDocId: '5e74f1a7ce17ae0041dfd056',
isConnected: true
}, },
argTypes: { argTypes: {
onInit: { action: 'onInit' }, onInit: { action: 'onInit' },

View file

@ -347,6 +347,18 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.disconnected-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
background: @file-tree-bg;
opacity: 0.5;
cursor: wait;
}
} }
.modal-new-file { .modal-new-file {

View file

@ -1,3 +1,4 @@
import { expect } from 'chai'
import React from 'react' import React from 'react'
import sinon from 'sinon' import sinon from 'sinon'
import { screen, render, fireEvent, waitFor } from '@testing-library/react' import { screen, render, fireEvent, waitFor } from '@testing-library/react'
@ -30,7 +31,7 @@ describe('<FileTreeRoot/>', function() {
fileRefs: [] fileRefs: []
} }
] ]
render( const { container } = render(
<FileTreeRoot <FileTreeRoot
rootFolder={rootFolder} rootFolder={rootFolder}
projectId="123abc" projectId="123abc"
@ -38,12 +39,14 @@ describe('<FileTreeRoot/>', function() {
rootDocId="456def" rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )
screen.queryByRole('tree') screen.queryByRole('tree')
screen.getByRole('treeitem') screen.getByRole('treeitem')
screen.getByRole('treeitem', { name: 'main.tex', selected: true }) screen.getByRole('treeitem', { name: 'main.tex', selected: true })
expect(container.querySelector('.disconnected-overlay')).to.not.exist
}) })
it('renders with invalid selected doc in local storage', async function() { it('renders with invalid selected doc in local storage', async function() {
@ -67,6 +70,7 @@ describe('<FileTreeRoot/>', function() {
rootDocId="456def" rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )
@ -80,6 +84,31 @@ describe('<FileTreeRoot/>', function() {
await waitFor(() => screen.getByRole('button', { name: 'Cancel' })) await waitFor(() => screen.getByRole('button', { name: 'Cancel' }))
}) })
it('renders disconnected overlay', function() {
const rootFolder = [
{
_id: 'root-folder-id',
docs: [{ _id: '456def', name: 'main.tex' }],
folders: [],
fileRefs: []
}
]
const { container } = render(
<FileTreeRoot
rootFolder={rootFolder}
projectId="123abc"
hasWritePermissions={false}
rootDocId="456def"
onSelect={onSelect}
onInit={onInit}
isConnected={false}
/>
)
expect(container.querySelector('.disconnected-overlay')).to.exist
})
it('fire onSelect', function() { it('fire onSelect', function() {
const rootFolder = [ const rootFolder = [
{ {
@ -100,6 +129,7 @@ describe('<FileTreeRoot/>', function() {
hasWritePermissions={false} hasWritePermissions={false}
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )
sinon.assert.calledOnce(onSelect) sinon.assert.calledOnce(onSelect)
@ -147,6 +177,7 @@ describe('<FileTreeRoot/>', function() {
hasWritePermissions={false} hasWritePermissions={false}
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )

View file

@ -26,6 +26,7 @@ describe('FileTree Context Menu Flow', function() {
rootDocId="456def" rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )
const treeitem = screen.getByRole('button', { name: 'main.tex' }) const treeitem = screen.getByRole('button', { name: 'main.tex' })
@ -54,6 +55,7 @@ describe('FileTree Context Menu Flow', function() {
rootDocId="456def" rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )
const treeitem = screen.getByRole('button', { name: 'main.tex' }) const treeitem = screen.getByRole('button', { name: 'main.tex' })

View file

@ -42,6 +42,7 @@ describe('FileTree Create Folder Flow', function() {
hasWritePermissions hasWritePermissions
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )
@ -97,6 +98,7 @@ describe('FileTree Create Folder Flow', function() {
rootDocId="789ghi" rootDocId="789ghi"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )
@ -161,6 +163,7 @@ describe('FileTree Create Folder Flow', function() {
rootDocId="456def" rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )
@ -214,6 +217,7 @@ describe('FileTree Create Folder Flow', function() {
rootDocId="456def" rootDocId="456def"
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )

View file

@ -41,6 +41,7 @@ describe('FileTree Delete Entity Flow', function() {
hasWritePermissions hasWritePermissions
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )
@ -139,6 +140,7 @@ describe('FileTree Delete Entity Flow', function() {
hasWritePermissions hasWritePermissions
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )
@ -187,6 +189,7 @@ describe('FileTree Delete Entity Flow', function() {
hasWritePermissions hasWritePermissions
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )

View file

@ -53,6 +53,7 @@ describe('FileTree Rename Entity Flow', function() {
hasWritePermissions hasWritePermissions
onSelect={onSelect} onSelect={onSelect}
onInit={onInit} onInit={onInit}
isConnected
/> />
) )
}) })