diff --git a/services/web/app/views/project/editor/file-tree-react.pug b/services/web/app/views/project/editor/file-tree-react.pug
index feb7b04bcc..e12523ebd9 100644
--- a/services/web/app/views/project/editor/file-tree-react.pug
+++ b/services/web/app/views/project/editor/file-tree-react.pug
@@ -18,6 +18,7 @@ aside.editor-sidebar.full-size(
has-write-permissions="hasWritePermissions"
on-select="onSelect"
on-init="onInit"
+ is-connected="isConnected"
)
div(ng-controller="FileTreeController")
diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-root.js b/services/web/frontend/js/features/file-tree/components/file-tree-root.js
index ffe9cdc0e0..ada036ed14 100644
--- a/services/web/frontend/js/features/file-tree/components/file-tree-root.js
+++ b/services/web/frontend/js/features/file-tree/components/file-tree-root.js
@@ -24,7 +24,8 @@ function FileTreeRoot({
rootDocId,
hasWritePermissions,
onSelect,
- onInit
+ onInit,
+ isConnected
}) {
const isReady = projectId && rootFolder
@@ -41,6 +42,7 @@ function FileTreeRoot({
rootDocId={rootDocId}
onSelect={onSelect}
>
+ {isConnected ? null :
}
@@ -83,7 +85,8 @@ FileTreeRoot.propTypes = {
rootDocId: PropTypes.string,
hasWritePermissions: PropTypes.bool.isRequired,
onSelect: PropTypes.func.isRequired,
- onInit: PropTypes.func.isRequired
+ onInit: PropTypes.func.isRequired,
+ isConnected: PropTypes.bool.isRequired
}
export default withErrorBoundary(FileTreeRoot, FileTreeError)
diff --git a/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js b/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js
index 0dd9a50f73..5b19524ae7 100644
--- a/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js
+++ b/services/web/frontend/js/features/file-tree/controllers/file-tree-controller.js
@@ -13,6 +13,7 @@ App.controller('ReactFileTreeController', function(
$scope.rootFolder = null
$scope.rootDocId = null
$scope.hasWritePermissions = false
+ $scope.isConnected = true
$scope.$on('project:joined', () => {
$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 = () => {
// 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
diff --git a/services/web/frontend/stories/file-tree.stories.js b/services/web/frontend/stories/file-tree.stories.js
index 61cb4c404b..5832a9c376 100644
--- a/services/web/frontend/stories/file-tree.stories.js
+++ b/services/web/frontend/stories/file-tree.stories.js
@@ -86,6 +86,9 @@ FullTree.parameters = { setupMocks: defaultSetupMocks }
export const ReadOnly = args =>
ReadOnly.args = { hasWritePermissions: false }
+export const Disconnected = args =>
+Disconnected.args = { isConnected: false }
+
export const NetworkErrors = args =>
NetworkErrors.parameters = {
setupMocks: () => {
@@ -116,7 +119,8 @@ export default {
rootFolder: rootFolderBase,
hasWritePermissions: true,
projectId: '123abc',
- rootDocId: '5e74f1a7ce17ae0041dfd056'
+ rootDocId: '5e74f1a7ce17ae0041dfd056',
+ isConnected: true
},
argTypes: {
onInit: { action: 'onInit' },
diff --git a/services/web/frontend/stylesheets/app/editor/file-tree.less b/services/web/frontend/stylesheets/app/editor/file-tree.less
index 53e4314fdc..88d8a6342c 100644
--- a/services/web/frontend/stylesheets/app/editor/file-tree.less
+++ b/services/web/frontend/stylesheets/app/editor/file-tree.less
@@ -347,6 +347,18 @@
text-overflow: ellipsis;
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 {
diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js b/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js
index d0415afe49..a02229c6da 100644
--- a/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js
+++ b/services/web/test/frontend/features/file-tree/components/file-tree-root.test.js
@@ -1,3 +1,4 @@
+import { expect } from 'chai'
import React from 'react'
import sinon from 'sinon'
import { screen, render, fireEvent, waitFor } from '@testing-library/react'
@@ -30,7 +31,7 @@ describe('', function() {
fileRefs: []
}
]
- render(
+ const { container } = render(
', function() {
rootDocId="456def"
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
screen.queryByRole('tree')
screen.getByRole('treeitem')
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() {
@@ -67,6 +70,7 @@ describe('', function() {
rootDocId="456def"
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
@@ -80,6 +84,31 @@ describe('', function() {
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(
+
+ )
+
+ expect(container.querySelector('.disconnected-overlay')).to.exist
+ })
+
it('fire onSelect', function() {
const rootFolder = [
{
@@ -100,6 +129,7 @@ describe('', function() {
hasWritePermissions={false}
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
sinon.assert.calledOnce(onSelect)
@@ -147,6 +177,7 @@ describe('', function() {
hasWritePermissions={false}
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
diff --git a/services/web/test/frontend/features/file-tree/flows/context-menu.test.js b/services/web/test/frontend/features/file-tree/flows/context-menu.test.js
index 5a2b85a45a..819429606e 100644
--- a/services/web/test/frontend/features/file-tree/flows/context-menu.test.js
+++ b/services/web/test/frontend/features/file-tree/flows/context-menu.test.js
@@ -26,6 +26,7 @@ describe('FileTree Context Menu Flow', function() {
rootDocId="456def"
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
const treeitem = screen.getByRole('button', { name: 'main.tex' })
@@ -54,6 +55,7 @@ describe('FileTree Context Menu Flow', function() {
rootDocId="456def"
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
const treeitem = screen.getByRole('button', { name: 'main.tex' })
diff --git a/services/web/test/frontend/features/file-tree/flows/create-folder.test.js b/services/web/test/frontend/features/file-tree/flows/create-folder.test.js
index af60cdb4b0..55d9a5006c 100644
--- a/services/web/test/frontend/features/file-tree/flows/create-folder.test.js
+++ b/services/web/test/frontend/features/file-tree/flows/create-folder.test.js
@@ -42,6 +42,7 @@ describe('FileTree Create Folder Flow', function() {
hasWritePermissions
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
@@ -97,6 +98,7 @@ describe('FileTree Create Folder Flow', function() {
rootDocId="789ghi"
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
@@ -161,6 +163,7 @@ describe('FileTree Create Folder Flow', function() {
rootDocId="456def"
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
@@ -214,6 +217,7 @@ describe('FileTree Create Folder Flow', function() {
rootDocId="456def"
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
diff --git a/services/web/test/frontend/features/file-tree/flows/delete-entity.test.js b/services/web/test/frontend/features/file-tree/flows/delete-entity.test.js
index 639bbe4788..5cd6291b9b 100644
--- a/services/web/test/frontend/features/file-tree/flows/delete-entity.test.js
+++ b/services/web/test/frontend/features/file-tree/flows/delete-entity.test.js
@@ -41,6 +41,7 @@ describe('FileTree Delete Entity Flow', function() {
hasWritePermissions
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
@@ -139,6 +140,7 @@ describe('FileTree Delete Entity Flow', function() {
hasWritePermissions
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
@@ -187,6 +189,7 @@ describe('FileTree Delete Entity Flow', function() {
hasWritePermissions
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
diff --git a/services/web/test/frontend/features/file-tree/flows/rename-entity.test.js b/services/web/test/frontend/features/file-tree/flows/rename-entity.test.js
index 586bd1728e..42200f3ea3 100644
--- a/services/web/test/frontend/features/file-tree/flows/rename-entity.test.js
+++ b/services/web/test/frontend/features/file-tree/flows/rename-entity.test.js
@@ -53,6 +53,7 @@ describe('FileTree Rename Entity Flow', function() {
hasWritePermissions
onSelect={onSelect}
onInit={onInit}
+ isConnected
/>
)
})