diff --git a/app/client/ui/DocMenu.ts b/app/client/ui/DocMenu.ts index 698ca980..d4aa094b 100644 --- a/app/client/ui/DocMenu.ts +++ b/app/client/ui/DocMenu.ts @@ -9,7 +9,7 @@ import {docUrl, urlState} from 'app/client/models/gristUrlState'; import {getTimeFromNow, HomeModel, makeLocalViewSettings, ViewSettings} from 'app/client/models/HomeModel'; import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo'; import * as css from 'app/client/ui/DocMenuCss'; -import {buildHomeIntro} from 'app/client/ui/HomeIntro'; +import {buildHomeIntro, buildWorkspaceIntro} from 'app/client/ui/HomeIntro'; import {buildUpgradeButton} from 'app/client/ui/ProductUpgrades'; import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs'; import {shadowScroll} from 'app/client/ui/shadowScroll'; @@ -134,12 +134,14 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) { dom('div', buildAllTemplates(home, home.templateWorkspaces, viewSettings) ) : - workspace && !workspace.isSupportWorkspace ? + workspace && !workspace.isSupportWorkspace && workspace.docs?.length ? css.docBlock( buildWorkspaceDocBlock(home, workspace, flashDocId, viewSettings), testId('doc-block') ) : - css.docBlock('Workspace not found') + workspace && !workspace.isSupportWorkspace && workspace.docs?.length === 0 ? + buildWorkspaceIntro(home) : + css.docBlock('Workspace not found') ) ]), ]; diff --git a/app/client/ui/HomeIntro.ts b/app/client/ui/HomeIntro.ts index 75debae4..a7ed60dd 100644 --- a/app/client/ui/HomeIntro.ts +++ b/app/client/ui/HomeIntro.ts @@ -32,6 +32,25 @@ export function buildHomeIntro(homeModel: HomeModel): DomContents { } } +export function buildWorkspaceIntro(homeModel: HomeModel): DomContents { + const isViewer = homeModel.currentWS.get()?.access === roles.VIEWER; + const isAnonym = !homeModel.app.currentValidUser; + const emptyLine = cssIntroLine(testId('empty-workspace-info'), "This workspace is empty."); + if (isAnonym || isViewer) { + return emptyLine; + } else { + return [ + emptyLine, + buildButtons(homeModel, { + invite: false, + templates: false, + import: true, + empty: true + }) + ]; + } +} + function makeViewerTeamSiteIntro(homeModel: HomeModel) { const personalOrg = Computed.create(null, use => use(homeModel.app.topAppModel.orgs).find(o => o.owner)); const docLink = (dom.maybe(personalOrg, org => { @@ -105,31 +124,46 @@ function helpCenterLink() { return cssLink({href: commonUrls.help, target: '_blank'}, cssInlineIcon('Help'), 'Help Center'); } - -function makeCreateButtons(homeModel: HomeModel) { - const canManageTeam = homeModel.app.isTeamSite && - roles.canEditAccess(homeModel.app.currentOrg?.access || null); +function buildButtons(homeModel: HomeModel, options: { + invite: boolean, + templates: boolean, + import: boolean, + empty: boolean, +}) { return cssBtnGroup( - (canManageTeam ? - cssBtn(cssBtnIcon('Help'), 'Invite Team Members', testId('intro-invite'), - cssButton.cls('-primary'), - dom.on('click', () => manageTeamUsersApp(homeModel.app)), - ) : - cssBtn(cssBtnIcon('FieldTable'), 'Browse Templates', testId('intro-templates'), - cssButton.cls('-primary'), - dom.hide(shouldHideUiElement("templates")), - urlState().setLinkUrl({homePage: 'templates'}), - ) + !options.invite ? null : + cssBtn(cssBtnIcon('Help'), 'Invite Team Members', testId('intro-invite'), + cssButton.cls('-primary'), + dom.on('click', () => manageTeamUsersApp(homeModel.app)), ), + !options.templates ? null : + cssBtn(cssBtnIcon('FieldTable'), 'Browse Templates', testId('intro-templates'), + cssButton.cls('-primary'), + dom.hide(shouldHideUiElement("templates")), + urlState().setLinkUrl({homePage: 'templates'}), + ), + !options.import ? null : cssBtn(cssBtnIcon('Import'), 'Import Document', testId('intro-import-doc'), dom.on('click', () => importDocAndOpen(homeModel)), ), + !options.empty ? null : cssBtn(cssBtnIcon('Page'), 'Create Empty Document', testId('intro-create-doc'), dom.on('click', () => createDocAndOpen(homeModel)), ), ); } +function makeCreateButtons(homeModel: HomeModel) { + const canManageTeam = homeModel.app.isTeamSite && + roles.canEditAccess(homeModel.app.currentOrg?.access || null); + return buildButtons(homeModel, { + invite: canManageTeam, + templates: !canManageTeam, + import: true, + empty: true + }); +} + const cssParagraph = styled(css.docBlock, ` color: ${theme.text}; line-height: 1.6; diff --git a/test/nbrowser/HomeIntro.ts b/test/nbrowser/HomeIntro.ts index 4eca33b1..75d82893 100644 --- a/test/nbrowser/HomeIntro.ts +++ b/test/nbrowser/HomeIntro.ts @@ -67,6 +67,7 @@ describe('HomeIntro', function() { it('should show examples workspace with the intro', testExamplesSection); it('should allow copying examples', testCopyingExamples.bind(null, undefined)); it('should render selected Examples workspace specially', testSelectedExamplesPage); + it('should show empty workspace info', testEmptyWorkspace.bind(null, {buttons: true})); }); describe("Logged-in on team site", function() { @@ -91,6 +92,7 @@ describe('HomeIntro', function() { it('should show examples workspace with the intro', testExamplesSection); it('should allow copying examples', testCopyingExamples.bind(null, gu.session().teamSite.orgName)); it('should render selected Examples workspace specially', testSelectedExamplesPage); + it('should show empty workspace info', testEmptyWorkspace); }); async function testOtherSitesSection() { @@ -118,13 +120,7 @@ describe('HomeIntro', function() { } async function testCreateImport(isLoggedIn: boolean) { - // Create doc from intro button - await driver.find('.test-intro-create-doc').click(); - await checkDocAndRestore(isLoggedIn, async () => assert.equal(await gu.getCell('A', 1).getText(), '')); - - // Import doc from intro button - await gu.fileDialogUpload('uploads/FileUploadData.csv', () => driver.find('.test-intro-import-doc').click()); - await checkDocAndRestore(isLoggedIn, async () => assert.equal(await gu.getCell('fname', 1).getText(), 'george')); + await checkIntroButtons(isLoggedIn); // Check that add-new menu has enabled Create Empty and Import Doc items. await driver.find('.test-dm-add-new').doClick(); @@ -158,29 +154,7 @@ describe('HomeIntro', function() { assert.isAbove(Number(await img.getAttribute('naturalWidth')), 0); }); - // Wait for doc to load, check it, then return to home page, and remove the doc so that we - // can see the intro again. - const checkDocAndRestore = async function(isLoggedIn: boolean, docChecker: () => Promise, - stepsBackToDocMenu: number = 1) { - await gu.waitForDocToLoad(); - await gu.dismissWelcomeTourIfNeeded(); - await docChecker(); - for (let i = 0; i < stepsBackToDocMenu; i++) { - await driver.navigate().back(); - } - await gu.waitForDocMenuToLoad(); - // If not logged in, we create docs "unsaved" and don't see them in doc-menu. - if (isLoggedIn) { - // Delete the first doc we find. We expect exactly one to exist. - assert.equal(await driver.find('.test-dm-doc').isPresent(), true); - await driver.find('.test-dm-doc').mouseMove().find('.test-dm-pinned-doc-options').click(); - await driver.find('.test-dm-delete-doc').click(); - await driver.find('.test-modal-confirm').click(); - await driver.wait(async () => !(await driver.find('.test-modal-dialog').isPresent()), 3000); - } - assert.equal(await driver.find('.test-dm-doc').isPresent(), false); - }; async function testExamplesCollapsing() { assert.equal(await driver.find('.test-dm-pinned-doc-name').isDisplayed(), true); @@ -314,3 +288,77 @@ describe('HomeIntro', function() { await checkImageLoaded(docItem.find('img')); } }); + +async function testEmptyWorkspace(options = { buttons: false }) { + await gu.openWorkspace("Home"); + assert.equal(await driver.findWait('.test-empty-workspace-info', 400).isDisplayed(), true); + // Create doc and check it is created. + await driver.find('.test-intro-create-doc').click(); + await waitAndDismiss(); + await emptyDockChecker(); + // Check that we don't see empty info. + await driver.navigate().back(); + await gu.waitForDocMenuToLoad(); + assert.equal(await driver.find('.test-empty-workspace-info').isPresent(), false); + // Remove created document, it also checks that document is visible. + await deleteFirstDoc(); + assert.equal(await driver.findWait('.test-empty-workspace-info', 400).isDisplayed(), true); + + await checkImportDocButton(true); +} + +// Wait for doc to load, check it, then return to home page, and remove the doc so that we +// can see the intro again. +async function checkDocAndRestore( + isLoggedIn: boolean, + docChecker: () => Promise, + stepsBackToDocMenu: number = 1) +{ + await waitAndDismiss(); + await docChecker(); + for (let i = 0; i < stepsBackToDocMenu; i++) { + await driver.navigate().back(); + } + await gu.waitForDocMenuToLoad(); + // If not logged in, we create docs "unsaved" and don't see them in doc-menu. + if (isLoggedIn) { + // Delete the first doc we find. We expect exactly one to exist. + await deleteFirstDoc(); + } + assert.equal(await driver.find('.test-dm-doc').isPresent(), false); +} + +async function waitAndDismiss() { + await gu.waitForDocToLoad(); + await gu.dismissWelcomeTourIfNeeded(); +} + +async function deleteFirstDoc() { + assert.equal(await driver.find('.test-dm-doc').isPresent(), true); + await driver.find('.test-dm-doc').mouseMove().find('.test-dm-pinned-doc-options').click(); + await driver.find('.test-dm-delete-doc').click(); + await driver.find('.test-modal-confirm').click(); + await driver.wait(async () => !(await driver.find('.test-modal-dialog').isPresent()), 3000); +} + +async function checkIntroButtons(isLoggedIn: boolean) { + // Create doc from intro button + await checkCreateDocButton(isLoggedIn); + + // Import doc from intro button + await checkImportDocButton(isLoggedIn); +} + +async function checkImportDocButton(isLoggedIn: boolean) { + await gu.fileDialogUpload('uploads/FileUploadData.csv', () => driver.find('.test-intro-import-doc').click()); + await checkDocAndRestore(isLoggedIn, async () => assert.equal(await gu.getCell('fname', 1).getText(), 'george')); +} + +async function checkCreateDocButton(isLoggedIn: boolean) { + await driver.find('.test-intro-create-doc').click(); + await checkDocAndRestore(isLoggedIn, emptyDockChecker); +} + +async function emptyDockChecker() { + assert.equal(await gu.getCell('A', 1).getText(), ''); +} diff --git a/test/nbrowser/gristUtils.ts b/test/nbrowser/gristUtils.ts index 997f1f8f..40867ddb 100644 --- a/test/nbrowser/gristUtils.ts +++ b/test/nbrowser/gristUtils.ts @@ -1471,6 +1471,12 @@ export async function openWsDropdown(wsName: string): Promise { await wsTab.find('.test-dm-workspace-options').mouseMove().click(); } +export async function openWorkspace(wsName: string): Promise { + const wsTab = await driver.findContentWait('.test-dm-workspace', wsName, 3000); + await wsTab.click(); + await waitForDocMenuToLoad(); +} + /** * Open ⋮ dropdown menu for named document. */