mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Update design of empty docs home page, and add a "Manage Team" button.
Summary: - Remove the empty-folder icon - Add an "Invite team members" button for owners on empty team sites - Add a "Browse Templates" button for all other cases on empty sites - Update intro text for team, including a link to Sprouts - Update intro text for personal/anon. - Include a Free/Pro tag for team sites (for now, only "Free") - Add a "Manage Team" button for owners on home page of all team sites. - Polished the UI of UserManager: add a transition for the background, and delay the appearance of the spinner for fast loads. Test Plan: Fixed up the HomeIntro tests; added test case for Manage Team button Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3459
This commit is contained in:
parent
af4738b94a
commit
acddd25cfd
@ -60,6 +60,8 @@ export interface AppModel {
|
|||||||
|
|
||||||
currentOrg: Organization|null; // null if no access to currentSubdomain
|
currentOrg: Organization|null; // null if no access to currentSubdomain
|
||||||
currentOrgName: string; // Our best guess for human-friendly name.
|
currentOrgName: string; // Our best guess for human-friendly name.
|
||||||
|
isPersonal: boolean; // Is it a personal site?
|
||||||
|
isTeamSite: boolean; // Is it a team site?
|
||||||
orgError?: OrgError; // If currentOrg is null, the error that caused it.
|
orgError?: OrgError; // If currentOrg is null, the error that caused it.
|
||||||
|
|
||||||
currentFeatures: Features; // features of the current org's product.
|
currentFeatures: Features; // features of the current org's product.
|
||||||
@ -180,6 +182,9 @@ export class AppModelImpl extends Disposable implements AppModel {
|
|||||||
// Figure out the org name, or blank if details are unavailable.
|
// Figure out the org name, or blank if details are unavailable.
|
||||||
public readonly currentOrgName = getOrgNameOrGuest(this.currentOrg, this.currentUser);
|
public readonly currentOrgName = getOrgNameOrGuest(this.currentOrg, this.currentUser);
|
||||||
|
|
||||||
|
public readonly isPersonal = Boolean(this.currentOrg?.owner);
|
||||||
|
public readonly isTeamSite = Boolean(this.currentOrg) && !this.isPersonal;
|
||||||
|
|
||||||
public readonly currentFeatures = (this.currentOrg && this.currentOrg.billingAccount) ?
|
public readonly currentFeatures = (this.currentOrg && this.currentOrg.billingAccount) ?
|
||||||
this.currentOrg.billingAccount.product.features : {};
|
this.currentOrg.billingAccount.product.features : {};
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import {loadGristDoc, loadUserManager} from 'app/client/lib/imports';
|
import {loadGristDoc} from 'app/client/lib/imports';
|
||||||
import {AppModel} from 'app/client/models/AppModel';
|
import {AppModel} from 'app/client/models/AppModel';
|
||||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||||
import {getLoginOrSignupUrl, getLoginUrl, getLogoutUrl, urlState} from 'app/client/models/gristUrlState';
|
import {getLoginOrSignupUrl, getLoginUrl, getLogoutUrl, urlState} from 'app/client/models/gristUrlState';
|
||||||
|
import {manageTeamUsers} from 'app/client/ui/OpenUserManager';
|
||||||
import {createUserImage} from 'app/client/ui/UserImage';
|
import {createUserImage} from 'app/client/ui/UserImage';
|
||||||
import * as viewport from 'app/client/ui/viewport';
|
import * as viewport from 'app/client/ui/viewport';
|
||||||
import {primaryButton} from 'app/client/ui2018/buttons';
|
import {primaryButton} from 'app/client/ui2018/buttons';
|
||||||
@ -11,7 +12,7 @@ import {menu, menuDivider, menuItem, menuItemLink, menuSubHeader} from 'app/clie
|
|||||||
import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls';
|
import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls';
|
||||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||||
import * as roles from 'app/common/roles';
|
import * as roles from 'app/common/roles';
|
||||||
import {Organization, SUPPORT_EMAIL} from 'app/common/UserAPI';
|
import {SUPPORT_EMAIL} from 'app/common/UserAPI';
|
||||||
import {Disposable, dom, DomElementArg, styled} from 'grainjs';
|
import {Disposable, dom, DomElementArg, styled} from 'grainjs';
|
||||||
import {cssMenuItem} from 'popweasel';
|
import {cssMenuItem} from 'popweasel';
|
||||||
import {maybeAddSiteSwitcherSection} from 'app/client/ui/SiteSwitcher';
|
import {maybeAddSiteSwitcherSection} from 'app/client/ui/SiteSwitcher';
|
||||||
@ -47,19 +48,6 @@ export class AccountWidget extends Disposable {
|
|||||||
* Note that `user` should NOT be anonymous (none of the items are really relevant).
|
* Note that `user` should NOT be anonymous (none of the items are really relevant).
|
||||||
*/
|
*/
|
||||||
private _makeAccountMenu(user: FullUser|null): DomElementArg[] {
|
private _makeAccountMenu(user: FullUser|null): DomElementArg[] {
|
||||||
// Opens the user-manager for the org.
|
|
||||||
// TODO: Factor out manageUsers, and related UI code, since AppHeader also uses it.
|
|
||||||
const manageUsers = async (org: Organization) => {
|
|
||||||
const api = this._appModel.api;
|
|
||||||
(await loadUserManager()).showUserManagerModal(api, {
|
|
||||||
permissionData: api.getOrgAccess(org.id),
|
|
||||||
activeUser: user,
|
|
||||||
resourceType: 'organization',
|
|
||||||
resourceId: org.id,
|
|
||||||
resource: org,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentOrg = this._appModel.currentOrg;
|
const currentOrg = this._appModel.currentOrg;
|
||||||
const gristDoc = this._docPageModel ? this._docPageModel.gristDoc.get() : null;
|
const gristDoc = this._docPageModel ? this._docPageModel.gristDoc.get() : null;
|
||||||
const isBillingManager = Boolean(currentOrg && currentOrg.billingAccount &&
|
const isBillingManager = Boolean(currentOrg && currentOrg.billingAccount &&
|
||||||
@ -104,8 +92,8 @@ export class AccountWidget extends Disposable {
|
|||||||
documentSettingsItem,
|
documentSettingsItem,
|
||||||
|
|
||||||
// Show 'Organization Settings' when on a home page of a valid org.
|
// Show 'Organization Settings' when on a home page of a valid org.
|
||||||
(!this._docPageModel && currentOrg && !currentOrg.owner ?
|
(!this._docPageModel && currentOrg && this._appModel.isTeamSite ?
|
||||||
menuItem(() => manageUsers(currentOrg),
|
menuItem(() => manageTeamUsers(currentOrg, user, this._appModel.api),
|
||||||
roles.canEditAccess(currentOrg.access) ? 'Manage Team' : 'Access Details',
|
roles.canEditAccess(currentOrg.access) ? 'Manage Team' : 'Access Details',
|
||||||
testId('dm-org-access')) :
|
testId('dm-org-access')) :
|
||||||
// Don't show on doc pages, or for personal orgs.
|
// Don't show on doc pages, or for personal orgs.
|
||||||
@ -113,7 +101,7 @@ export class AccountWidget extends Disposable {
|
|||||||
|
|
||||||
shouldHideUiElement("billing") ? null :
|
shouldHideUiElement("billing") ? null :
|
||||||
// Show link to billing pages.
|
// Show link to billing pages.
|
||||||
currentOrg && !currentOrg.owner ?
|
this._appModel.isTeamSite ?
|
||||||
// For links, disabling with just a class is hard; easier to just not make it a link.
|
// For links, disabling with just a class is hard; easier to just not make it a link.
|
||||||
// TODO weasel menus should support disabling menuItemLink.
|
// TODO weasel menus should support disabling menuItemLink.
|
||||||
(isBillingManager ?
|
(isBillingManager ?
|
||||||
|
@ -6,14 +6,25 @@ import {shouldHideUiElement} from 'app/common/gristUrls';
|
|||||||
import * as version from 'app/common/version';
|
import * as version from 'app/common/version';
|
||||||
import {BindableValue, Disposable, dom, styled} from "grainjs";
|
import {BindableValue, Disposable, dom, styled} from "grainjs";
|
||||||
import {menu, menuItem, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus';
|
import {menu, menuItem, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus';
|
||||||
import {Organization, SUPPORT_EMAIL} from 'app/common/UserAPI';
|
import {isTemplatesOrg, Organization, SUPPORT_EMAIL} from 'app/common/UserAPI';
|
||||||
import {AppModel} from 'app/client/models/AppModel';
|
import {AppModel} from 'app/client/models/AppModel';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||||
import * as roles from 'app/common/roles';
|
import * as roles from 'app/common/roles';
|
||||||
import {loadUserManager} from 'app/client/lib/imports';
|
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
|
||||||
import {maybeAddSiteSwitcherSection} from 'app/client/ui/SiteSwitcher';
|
import {maybeAddSiteSwitcherSection} from 'app/client/ui/SiteSwitcher';
|
||||||
|
import {DomContents} from 'grainjs';
|
||||||
|
|
||||||
|
// Maps a name of a Product (from app/gen-server/entity/Product.ts) to a tag (pill) to show next
|
||||||
|
// to the org name.
|
||||||
|
const productPills: {[name: string]: string|null} = {
|
||||||
|
// TODO We don't label paid team plans with a tag yet, but we should label as "Pro" once we
|
||||||
|
// update our pricing pages to refer to paid team plans as Pro plans.
|
||||||
|
"professional": null, // Deprecated but used in development.
|
||||||
|
"team": null, // Used for the paid team plans.
|
||||||
|
"teamFree": "Free", // The new free team plan.
|
||||||
|
// Other plans are either personal, or grandfathered, or for testing.
|
||||||
|
};
|
||||||
|
|
||||||
export class AppHeader extends Disposable {
|
export class AppHeader extends Disposable {
|
||||||
constructor(private _orgName: BindableValue<string>, private _appModel: AppModel,
|
constructor(private _orgName: BindableValue<string>, private _appModel: AppModel,
|
||||||
@ -26,22 +37,9 @@ export class AppHeader extends Disposable {
|
|||||||
|
|
||||||
const user = this._appModel.currentValidUser;
|
const user = this._appModel.currentValidUser;
|
||||||
const currentOrg = this._appModel.currentOrg;
|
const currentOrg = this._appModel.currentOrg;
|
||||||
const isTeamSite = Boolean(currentOrg && !currentOrg.owner);
|
|
||||||
const isBillingManager = Boolean(currentOrg && currentOrg.billingAccount &&
|
const isBillingManager = Boolean(currentOrg && currentOrg.billingAccount &&
|
||||||
(currentOrg.billingAccount.isManager || user?.email === SUPPORT_EMAIL));
|
(currentOrg.billingAccount.isManager || user?.email === SUPPORT_EMAIL));
|
||||||
|
|
||||||
// Opens the user-manager for the org.
|
|
||||||
const manageUsers = async (org: Organization) => {
|
|
||||||
const api = this._appModel.api;
|
|
||||||
(await loadUserManager()).showUserManagerModal(api, {
|
|
||||||
permissionData: api.getOrgAccess(org.id),
|
|
||||||
activeUser: user,
|
|
||||||
resourceType: 'organization',
|
|
||||||
resourceId: org.id,
|
|
||||||
resource: org
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return cssAppHeader(
|
return cssAppHeader(
|
||||||
cssAppHeader.cls('-widelogo', theme.wideLogo || false),
|
cssAppHeader.cls('-widelogo', theme.wideLogo || false),
|
||||||
// Show version when hovering over the application icon.
|
// Show version when hovering over the application icon.
|
||||||
@ -51,15 +49,17 @@ export class AppHeader extends Disposable {
|
|||||||
testId('dm-logo')
|
testId('dm-logo')
|
||||||
),
|
),
|
||||||
cssOrg(
|
cssOrg(
|
||||||
cssOrgName(dom.text(this._orgName)),
|
cssOrgName(dom.text(this._orgName), testId('dm-orgname')),
|
||||||
|
productPill(currentOrg),
|
||||||
this._orgName && cssDropdownIcon('Dropdown'),
|
this._orgName && cssDropdownIcon('Dropdown'),
|
||||||
menu(() => [
|
menu(() => [
|
||||||
menuSubHeader(`${isTeamSite ? 'Team' : 'Personal'} Site`, testId('orgmenu-title')),
|
menuSubHeader(`${this._appModel.isTeamSite ? 'Team' : 'Personal'} Site`, testId('orgmenu-title')),
|
||||||
menuItemLink(urlState().setLinkUrl({}), 'Home Page', testId('orgmenu-home-page')),
|
menuItemLink(urlState().setLinkUrl({}), 'Home Page', testId('orgmenu-home-page')),
|
||||||
|
|
||||||
// Show 'Organization Settings' when on a home page of a valid org.
|
// Show 'Organization Settings' when on a home page of a valid org.
|
||||||
(!this._docPageModel && currentOrg && !currentOrg.owner ?
|
(!this._docPageModel && currentOrg && !currentOrg.owner ?
|
||||||
menuItem(() => manageUsers(currentOrg), 'Manage Team', testId('orgmenu-manage-team'),
|
menuItem(() => manageTeamUsersApp(this._appModel),
|
||||||
|
'Manage Team', testId('orgmenu-manage-team'),
|
||||||
dom.cls('disabled', !roles.canEditAccess(currentOrg.access))) :
|
dom.cls('disabled', !roles.canEditAccess(currentOrg.access))) :
|
||||||
// Don't show on doc pages, or for personal orgs.
|
// Don't show on doc pages, or for personal orgs.
|
||||||
null),
|
null),
|
||||||
@ -82,6 +82,22 @@ export class AppHeader extends Disposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function productPill(org: Organization|null, options: {large?: boolean} = {}): DomContents {
|
||||||
|
if (!org || isTemplatesOrg(org)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const product = org?.billingAccount?.product.name;
|
||||||
|
const pillTag = product && productPills[product];
|
||||||
|
if (!pillTag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return cssProductPill(cssProductPill.cls('-' + pillTag),
|
||||||
|
options.large ? cssProductPill.cls('-large') : null,
|
||||||
|
pillTag,
|
||||||
|
testId('appheader-product-pill'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const cssAppHeader = styled('div', `
|
const cssAppHeader = styled('div', `
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -126,6 +142,11 @@ const cssOrg = styled('div', `
|
|||||||
max-width: calc(100% - 48px);
|
max-width: calc(100% - 48px);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${colors.mediumGrey};
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssOrgName = styled('div', `
|
const cssOrgName = styled('div', `
|
||||||
@ -138,3 +159,25 @@ const cssOrgName = styled('div', `
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssProductPill = styled('div', `
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: ${vars.smallFontSize};
|
||||||
|
padding: 2px 4px;
|
||||||
|
display: inline;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
&-Free {
|
||||||
|
background-color: ${colors.orange};
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
&-Pro {
|
||||||
|
background-color: ${colors.lightGreen};
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
&-large {
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-left: 16px;
|
||||||
|
font-size: ${vars.mediumFontSize};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
@ -67,7 +67,9 @@ function createLoadedDocMenu(home: HomeModel) {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
// Hide the sort option only when showing intro.
|
// Hide the sort option only when showing intro.
|
||||||
buildPrefs(viewSettings, {hideSort: showIntro}),
|
((showIntro && page === 'all') ? null :
|
||||||
|
buildPrefs(viewSettings, {hideSort: showIntro})
|
||||||
|
),
|
||||||
|
|
||||||
// Build the pinned docs dom. Builds nothing if the selectedOrg is unloaded or
|
// Build the pinned docs dom. Builds nothing if the selectedOrg is unloaded or
|
||||||
dom.maybe((use) => use(home.currentWSPinnedDocs).length > 0, () => [
|
dom.maybe((use) => use(home.currentWSPinnedDocs).length > 0, () => [
|
||||||
|
@ -33,7 +33,7 @@ export const docList = styled('div', `
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
export const docListHeader = styled('div', `
|
export const docListHeader = styled('div', `
|
||||||
height: 32px;
|
min-height: 32px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
color: ${colors.dark};
|
color: ${colors.dark};
|
||||||
|
@ -1,63 +1,80 @@
|
|||||||
import {getLoginOrSignupUrl} from 'app/client/models/gristUrlState';
|
import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
||||||
import {HomeModel} from 'app/client/models/HomeModel';
|
import {HomeModel} from 'app/client/models/HomeModel';
|
||||||
|
import {productPill} from 'app/client/ui/AppHeader';
|
||||||
import * as css from 'app/client/ui/DocMenuCss';
|
import * as css from 'app/client/ui/DocMenuCss';
|
||||||
import {createDocAndOpen, importDocAndOpen} from 'app/client/ui/HomeLeftPane';
|
import {createDocAndOpen, importDocAndOpen} from 'app/client/ui/HomeLeftPane';
|
||||||
import {bigBasicButton} from 'app/client/ui2018/buttons';
|
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
|
||||||
import {mediaXSmall, testId} from 'app/client/ui2018/cssVars';
|
import {bigBasicButton, cssButton} from 'app/client/ui2018/buttons';
|
||||||
|
import {testId, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {cssLink} from 'app/client/ui2018/links';
|
import {cssLink} from 'app/client/ui2018/links';
|
||||||
import {commonUrls} from 'app/common/gristUrls';
|
import {commonUrls} from 'app/common/gristUrls';
|
||||||
import {dom, DomContents, DomCreateFunc, styled} from 'grainjs';
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||||
|
import * as roles from 'app/common/roles';
|
||||||
|
import {dom, DomContents, styled} from 'grainjs';
|
||||||
|
|
||||||
export function buildHomeIntro(homeModel: HomeModel): DomContents {
|
export function buildHomeIntro(homeModel: HomeModel): DomContents {
|
||||||
const user = homeModel.app.currentValidUser;
|
const user = homeModel.app.currentValidUser;
|
||||||
if (user) {
|
if (user) {
|
||||||
return [
|
return homeModel.app.isTeamSite ? makeTeamSiteIntro(homeModel) : makePersonalIntro(homeModel, user);
|
||||||
css.docListHeader(`Welcome to Grist, ${user.name}!`, testId('welcome-title')),
|
|
||||||
cssIntroSplit(
|
|
||||||
cssIntroLeft(
|
|
||||||
cssIntroImage({src: 'https://www.getgrist.com/themes/grist/assets/images/empty-folder.png'}),
|
|
||||||
testId('intro-image'),
|
|
||||||
),
|
|
||||||
cssIntroRight(
|
|
||||||
cssParagraph(
|
|
||||||
'Watch video on ',
|
|
||||||
cssLink({href: 'https://support.getgrist.com/creating-doc/', target: '_blank'}, 'creating a document'),
|
|
||||||
'.', dom('br'),
|
|
||||||
'Learn more in our ', cssLink({href: commonUrls.help, target: '_blank'}, 'Help Center'), '.',
|
|
||||||
testId('welcome-text')
|
|
||||||
),
|
|
||||||
makeCreateButtons(homeModel),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
} else {
|
} else {
|
||||||
return [
|
return makeAnonIntro(homeModel);
|
||||||
cssIntroSplit(
|
|
||||||
cssIntroLeft(
|
|
||||||
cssLink({href: 'https://support.getgrist.com/creating-doc/', target: '_blank'},
|
|
||||||
cssIntroImage({src: 'https://www.getgrist.com/themes/grist/assets/images/video-create-doc.png'}),
|
|
||||||
),
|
|
||||||
testId('intro-image'),
|
|
||||||
),
|
|
||||||
cssIntroRight(
|
|
||||||
css.docListHeader('Welcome to Grist!', testId('welcome-title')),
|
|
||||||
cssParagraph(
|
|
||||||
'You can explore and experiment without logging in. ',
|
|
||||||
'To save your work, however, you’ll need to ',
|
|
||||||
cssLink({href: getLoginOrSignupUrl()}, 'sign up'), '.', dom('br'),
|
|
||||||
'Learn more in our ', cssLink({href: commonUrls.help, target: '_blank'}, 'Help Center'), '.',
|
|
||||||
testId('welcome-text')
|
|
||||||
),
|
|
||||||
makeCreateButtons(homeModel),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeTeamSiteIntro(homeModel: HomeModel) {
|
||||||
|
const sproutsProgram = cssLink({href: commonUrls.sproutsProgram, target: '_blank'}, 'Sprouts Program');
|
||||||
|
return [
|
||||||
|
css.docListHeader(`Welcome to ${homeModel.app.currentOrgName}`,
|
||||||
|
productPill(homeModel.app.currentOrg, {large: true}),
|
||||||
|
testId('welcome-title')),
|
||||||
|
cssIntroLine('Get started by inviting your team and creating your first Grist document.'),
|
||||||
|
cssIntroLine('Learn more in our ', helpCenterLink(), ', or find an expert via our ', sproutsProgram, '.',
|
||||||
|
testId('welcome-text')),
|
||||||
|
makeCreateButtons(homeModel),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function makePersonalIntro(homeModel: HomeModel, user: FullUser) {
|
||||||
|
return [
|
||||||
|
css.docListHeader(`Welcome to Grist, ${user.name}!`, testId('welcome-title')),
|
||||||
|
cssIntroLine('Get started by creating your first Grist document.'),
|
||||||
|
cssIntroLine('Visit our ', helpCenterLink(), ' to learn more.',
|
||||||
|
testId('welcome-text')),
|
||||||
|
makeCreateButtons(homeModel),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeAnonIntro(homeModel: HomeModel) {
|
||||||
|
const signUp = cssLink({href: getLoginOrSignupUrl()}, 'Sign up');
|
||||||
|
return [
|
||||||
|
css.docListHeader(`Welcome to Grist!`, testId('welcome-title')),
|
||||||
|
cssIntroLine('Get started by exploring templates, or creating your first Grist document.'),
|
||||||
|
cssIntroLine(signUp, ' to save your work. Visit our ', helpCenterLink(), ' to learn more.',
|
||||||
|
testId('welcome-text')),
|
||||||
|
makeCreateButtons(homeModel),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function helpCenterLink() {
|
||||||
|
return cssLink({href: commonUrls.help, target: '_blank'}, cssInlineIcon('Help'), 'Help Center');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function makeCreateButtons(homeModel: HomeModel) {
|
function makeCreateButtons(homeModel: HomeModel) {
|
||||||
|
const canManageTeam = homeModel.app.isTeamSite &&
|
||||||
|
roles.canEditAccess(homeModel.app.currentOrg?.access || null);
|
||||||
return cssBtnGroup(
|
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'),
|
||||||
|
urlState().setLinkUrl({homePage: 'templates'}),
|
||||||
|
)
|
||||||
|
),
|
||||||
cssBtn(cssBtnIcon('Import'), 'Import Document', testId('intro-import-doc'),
|
cssBtn(cssBtnIcon('Import'), 'Import Document', testId('intro-import-doc'),
|
||||||
dom.on('click', () => importDocAndOpen(homeModel)),
|
dom.on('click', () => importDocAndOpen(homeModel)),
|
||||||
),
|
),
|
||||||
@ -67,44 +84,24 @@ function makeCreateButtons(homeModel: HomeModel) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssIntroSplit = styled(css.docBlock, `
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@media ${mediaXSmall} {
|
|
||||||
& {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssIntroLeft = styled('div', `
|
|
||||||
flex: 0.4 1 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
max-height: 150px;
|
|
||||||
text-align: center;
|
|
||||||
margin: 32px 0;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssIntroRight = styled('div', `
|
|
||||||
flex: 0.6 1 0px;
|
|
||||||
overflow: auto;
|
|
||||||
margin-left: 8px;
|
|
||||||
`);
|
|
||||||
|
|
||||||
const cssParagraph = styled(css.docBlock, `
|
const cssParagraph = styled(css.docBlock, `
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssIntroLine = styled(cssParagraph, `
|
||||||
|
font-size: ${vars.introFontSize};
|
||||||
|
margin-bottom: 8px;
|
||||||
|
`);
|
||||||
|
|
||||||
const cssBtnGroup = styled('div', `
|
const cssBtnGroup = styled('div', `
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
margin-top: -16px;
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssBtn = styled(bigBasicButton, `
|
const cssBtn = styled(bigBasicButton, `
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -114,11 +111,6 @@ const cssBtnIcon = styled(icon, `
|
|||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Helper to create an image scaled down to half of its intrinsic size.
|
const cssInlineIcon = styled(icon, `
|
||||||
// Based on https://stackoverflow.com/a/25026615/328565
|
margin: -2px 4px 2px 4px;
|
||||||
const cssIntroImage: DomCreateFunc<HTMLDivElement> =
|
`);
|
||||||
(...args) => _cssImageWrap1(_cssImageWrap2(_cssImageScaled(...args)));
|
|
||||||
|
|
||||||
const _cssImageWrap1 = styled('div', `width: 200%; margin-left: -50%;`);
|
|
||||||
const _cssImageWrap2 = styled('div', `display: inline-block;`);
|
|
||||||
const _cssImageScaled = styled('img', `width: 50%;`);
|
|
||||||
|
@ -14,7 +14,7 @@ import {select} from 'app/client/ui2018/menus';
|
|||||||
import {confirmModal, cssModalBody, cssModalButtons, cssModalWidth, modal, saveModal} from 'app/client/ui2018/modals';
|
import {confirmModal, cssModalBody, cssModalButtons, cssModalWidth, modal, saveModal} from 'app/client/ui2018/modals';
|
||||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||||
import * as roles from 'app/common/roles';
|
import * as roles from 'app/common/roles';
|
||||||
import {Document, Organization, Workspace} from 'app/common/UserAPI';
|
import {Document, isTemplatesOrg, Organization, Workspace} from 'app/common/UserAPI';
|
||||||
import {Computed, Disposable, dom, input, Observable, styled, subscribe} from 'grainjs';
|
import {Computed, Disposable, dom, input, Observable, styled, subscribe} from 'grainjs';
|
||||||
import sortBy = require('lodash/sortBy');
|
import sortBy = require('lodash/sortBy');
|
||||||
|
|
||||||
@ -99,10 +99,9 @@ export async function makeCopy(doc: Document, app: AppModel, modalTitle: string)
|
|||||||
}
|
}
|
||||||
let orgs = allowOtherOrgs(doc, app) ? await app.api.getOrgs(true) : null;
|
let orgs = allowOtherOrgs(doc, app) ? await app.api.getOrgs(true) : null;
|
||||||
if (orgs) {
|
if (orgs) {
|
||||||
// TODO: Need a more robust way to detect and exclude the templates org.
|
|
||||||
// Don't show the templates org since it's selected by default, and
|
// Don't show the templates org since it's selected by default, and
|
||||||
// is not writable to.
|
// is not writable to.
|
||||||
orgs = orgs.filter(o => o.domain !== 'templates' && o.domain !== 'templates-s');
|
orgs = orgs.filter(o => !isTemplatesOrg(o));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a dialog with a form to select destination.
|
// Show a dialog with a form to select destination.
|
||||||
|
21
app/client/ui/OpenUserManager.ts
Normal file
21
app/client/ui/OpenUserManager.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import {loadUserManager} from 'app/client/lib/imports';
|
||||||
|
import {AppModel} from 'app/client/models/AppModel';
|
||||||
|
import {FullUser, Organization, UserAPI} from 'app/common/UserAPI';
|
||||||
|
|
||||||
|
// Opens the user-manager for the given org.
|
||||||
|
export async function manageTeamUsers(org: Organization, user: FullUser|null, api: UserAPI) {
|
||||||
|
(await loadUserManager()).showUserManagerModal(api, {
|
||||||
|
permissionData: api.getOrgAccess(org.id),
|
||||||
|
activeUser: user,
|
||||||
|
resourceType: 'organization',
|
||||||
|
resourceId: org.id,
|
||||||
|
resource: org,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opens the user-manager for the current org in the given AppModel.
|
||||||
|
export async function manageTeamUsersApp(app: AppModel) {
|
||||||
|
if (app.currentOrg) {
|
||||||
|
return manageTeamUsers(app.currentOrg, app.currentValidUser, app.api);
|
||||||
|
}
|
||||||
|
}
|
@ -5,17 +5,33 @@ import {DocPageModel} from 'app/client/models/DocPageModel';
|
|||||||
import {workspaceName} from 'app/client/models/WorkspaceInfo';
|
import {workspaceName} from 'app/client/models/WorkspaceInfo';
|
||||||
import {AccountWidget} from 'app/client/ui/AccountWidget';
|
import {AccountWidget} from 'app/client/ui/AccountWidget';
|
||||||
import {buildNotifyMenuButton} from 'app/client/ui/NotifyUI';
|
import {buildNotifyMenuButton} from 'app/client/ui/NotifyUI';
|
||||||
|
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
|
||||||
import {buildShareMenuButton} from 'app/client/ui/ShareMenu';
|
import {buildShareMenuButton} from 'app/client/ui/ShareMenu';
|
||||||
import {cssHoverCircle, cssTopBarBtn} from 'app/client/ui/TopBarCss';
|
import {cssHoverCircle, cssTopBarBtn} from 'app/client/ui/TopBarCss';
|
||||||
import {docBreadcrumbs} from 'app/client/ui2018/breadcrumbs';
|
import {docBreadcrumbs} from 'app/client/ui2018/breadcrumbs';
|
||||||
|
import {basicButton} from 'app/client/ui2018/buttons';
|
||||||
import {colors, cssHideForNarrowScreen, testId} from 'app/client/ui2018/cssVars';
|
import {colors, cssHideForNarrowScreen, testId} from 'app/client/ui2018/cssVars';
|
||||||
import {IconName} from 'app/client/ui2018/IconList';
|
import {IconName} from 'app/client/ui2018/IconList';
|
||||||
import {waitGrainObs} from 'app/common/gutil';
|
import {waitGrainObs} from 'app/common/gutil';
|
||||||
|
import * as roles from 'app/common/roles';
|
||||||
import {Computed, dom, DomElementArg, makeTestId, MultiHolder, Observable, styled} from 'grainjs';
|
import {Computed, dom, DomElementArg, makeTestId, MultiHolder, Observable, styled} from 'grainjs';
|
||||||
|
|
||||||
export function createTopBarHome(appModel: AppModel) {
|
export function createTopBarHome(appModel: AppModel) {
|
||||||
return [
|
return [
|
||||||
cssFlexSpace(),
|
cssFlexSpace(),
|
||||||
|
|
||||||
|
(appModel.isTeamSite && roles.canEditAccess(appModel.currentOrg?.access || null) ?
|
||||||
|
[
|
||||||
|
basicButton(
|
||||||
|
'Manage Team',
|
||||||
|
dom.on('click', () => manageTeamUsersApp(appModel)),
|
||||||
|
testId('topbar-manage-team')
|
||||||
|
),
|
||||||
|
cssSpacer()
|
||||||
|
] :
|
||||||
|
null
|
||||||
|
),
|
||||||
|
|
||||||
buildNotifyMenuButton(appModel.notifier, appModel),
|
buildNotifyMenuButton(appModel.notifier, appModel),
|
||||||
dom('div', dom.create(AccountWidget, appModel)),
|
dom('div', dom.create(AccountWidget, appModel)),
|
||||||
];
|
];
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
* It can be instantiated by calling showUserManagerModal with the UserAPI and IUserManagerOptions.
|
* It can be instantiated by calling showUserManagerModal with the UserAPI and IUserManagerOptions.
|
||||||
*/
|
*/
|
||||||
import {commonUrls} from 'app/common/gristUrls';
|
import {commonUrls} from 'app/common/gristUrls';
|
||||||
|
import {isLongerThan} from 'app/common/gutil';
|
||||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||||
import * as roles from 'app/common/roles';
|
import * as roles from 'app/common/roles';
|
||||||
import {Organization, PermissionData, UserAPI} from 'app/common/UserAPI';
|
import {Organization, PermissionData, UserAPI} from 'app/common/UserAPI';
|
||||||
@ -71,11 +72,11 @@ async function getModel(options: IUserManagerOptions): Promise<UserManagerModelI
|
|||||||
* the UserManager menu with save and cancel buttons.
|
* the UserManager menu with save and cancel buttons.
|
||||||
*/
|
*/
|
||||||
export function showUserManagerModal(userApi: UserAPI, options: IUserManagerOptions) {
|
export function showUserManagerModal(userApi: UserAPI, options: IUserManagerOptions) {
|
||||||
const modelObs: Observable<UserManagerModel|null> = observable(null);
|
const modelObs: Observable<UserManagerModel|null|"slow"> = observable(null);
|
||||||
|
|
||||||
async function onConfirm(ctl: IModalControl) {
|
async function onConfirm(ctl: IModalControl) {
|
||||||
const model = modelObs.get();
|
const model = modelObs.get();
|
||||||
if (!model) {
|
if (!model || model === "slow") {
|
||||||
ctl.close();
|
ctl.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -111,15 +112,17 @@ export function showUserManagerModal(userApi: UserAPI, options: IUserManagerOpti
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the model and assign it to the observable. Report errors to the app.
|
// Get the model and assign it to the observable. Report errors to the app.
|
||||||
getModel(options)
|
const waitPromise = getModel(options)
|
||||||
.then(model => modelObs.set(model))
|
.then(model => modelObs.set(model))
|
||||||
.catch(reportError);
|
.catch(reportError);
|
||||||
|
|
||||||
|
isLongerThan(waitPromise, 400).then((slow) => slow && modelObs.set("slow")).catch(() => {});
|
||||||
|
|
||||||
return buildUserManagerModal(modelObs, onConfirm, options);
|
return buildUserManagerModal(modelObs, onConfirm, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUserManagerModal(
|
function buildUserManagerModal(
|
||||||
modelObs: Observable<UserManagerModel|null>,
|
modelObs: Observable<UserManagerModel|null|"slow">,
|
||||||
onConfirm: (ctl: IModalControl) => Promise<void>,
|
onConfirm: (ctl: IModalControl) => Promise<void>,
|
||||||
options: IUserManagerOptions
|
options: IUserManagerOptions
|
||||||
) {
|
) {
|
||||||
@ -127,19 +130,20 @@ function buildUserManagerModal(
|
|||||||
// We set the padding to 0 since the body scroll shadows extend to the edge of the modal.
|
// We set the padding to 0 since the body scroll shadows extend to the edge of the modal.
|
||||||
{ style: 'padding: 0;' },
|
{ style: 'padding: 0;' },
|
||||||
options.showAnimation ? dom.cls(cssAnimatedModal.className) : null,
|
options.showAnimation ? dom.cls(cssAnimatedModal.className) : null,
|
||||||
dom.maybe(modelObs, model => cssTitle(
|
dom.domComputed(modelObs, model => {
|
||||||
|
if (!model) { return null; }
|
||||||
|
if (model === "slow") { return cssSpinner(loadingSpinner()); }
|
||||||
|
|
||||||
|
const cssBody = model.isPersonal ? cssAccessDetailsBody : cssUserManagerBody;
|
||||||
|
return [
|
||||||
|
cssTitle(
|
||||||
renderTitle(options.resourceType, options.resource, model.isPersonal),
|
renderTitle(options.resourceType, options.resource, model.isPersonal),
|
||||||
(options.resourceType === 'document' && (!model.isPersonal || model.isPublicMember)
|
(options.resourceType === 'document' && (!model.isPersonal || model.isPublicMember)
|
||||||
? makeCopyBtn(options.linkToCopy, cssCopyBtn.cls('-header'))
|
? makeCopyBtn(options.linkToCopy, cssCopyBtn.cls('-header'))
|
||||||
: null
|
: null
|
||||||
),
|
),
|
||||||
testId('um-header'),
|
testId('um-header'),
|
||||||
)),
|
),
|
||||||
dom.domComputed(modelObs, model => {
|
|
||||||
if (!model) { return cssSpinner(loadingSpinner()); }
|
|
||||||
|
|
||||||
const cssBody = model.isPersonal ? cssAccessDetailsBody : cssUserManagerBody;
|
|
||||||
return [
|
|
||||||
cssModalBody(
|
cssModalBody(
|
||||||
cssBody(
|
cssBody(
|
||||||
new UserManager(
|
new UserManager(
|
||||||
|
@ -39,7 +39,8 @@ export const cssButton = styled('button', `
|
|||||||
|
|
||||||
&-large {
|
&-large {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 12px 24px;
|
padding: 10px 24px;
|
||||||
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-primary {
|
&-primary {
|
||||||
|
@ -43,6 +43,7 @@ export const colors = {
|
|||||||
|
|
||||||
lighterBlue: new CustomProp('color-lighter-blue', '#87b2f9'),
|
lighterBlue: new CustomProp('color-lighter-blue', '#87b2f9'),
|
||||||
lightBlue: new CustomProp('color-light-blue', '#3B82F6'),
|
lightBlue: new CustomProp('color-light-blue', '#3B82F6'),
|
||||||
|
orange: new CustomProp('color-orange', '#F9AE41'),
|
||||||
|
|
||||||
cursor: new CustomProp('color-cursor', '#16B378'), // cursor is lightGreen
|
cursor: new CustomProp('color-cursor', '#16B378'), // cursor is lightGreen
|
||||||
selection: new CustomProp('color-selection', 'rgba(22,179,120,0.15)'),
|
selection: new CustomProp('color-selection', 'rgba(22,179,120,0.15)'),
|
||||||
@ -74,6 +75,7 @@ export const vars = {
|
|||||||
xsmallFontSize: new CustomProp('x-small-font-size', '10px'),
|
xsmallFontSize: new CustomProp('x-small-font-size', '10px'),
|
||||||
smallFontSize: new CustomProp('small-font-size', '11px'),
|
smallFontSize: new CustomProp('small-font-size', '11px'),
|
||||||
mediumFontSize: new CustomProp('medium-font-size', '13px'),
|
mediumFontSize: new CustomProp('medium-font-size', '13px'),
|
||||||
|
introFontSize: new CustomProp('intro-font-size', '14px'), // feels friendlier
|
||||||
largeFontSize: new CustomProp('large-font-size', '16px'),
|
largeFontSize: new CustomProp('large-font-size', '16px'),
|
||||||
xlargeFontSize: new CustomProp('x-large-font-size', '18px'),
|
xlargeFontSize: new CustomProp('x-large-font-size', '18px'),
|
||||||
xxlargeFontSize: new CustomProp('xx-large-font-size', '20px'),
|
xxlargeFontSize: new CustomProp('xx-large-font-size', '20px'),
|
||||||
|
@ -5,7 +5,8 @@ import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/but
|
|||||||
import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars';
|
import {colors, mediaSmall, testId, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {loadingSpinner} from 'app/client/ui2018/loaders';
|
import {loadingSpinner} from 'app/client/ui2018/loaders';
|
||||||
import {waitGrainObs} from 'app/common/gutil';
|
import {waitGrainObs} from 'app/common/gutil';
|
||||||
import {Computed, Disposable, dom, DomContents, DomElementArg, input, MultiHolder, Observable, styled} from 'grainjs';
|
import {Computed, Disposable, dom, DomContents, DomElementArg, input, keyframes,
|
||||||
|
MultiHolder, Observable, styled} from 'grainjs';
|
||||||
|
|
||||||
// IModalControl is passed into the function creating the body of the modal.
|
// IModalControl is passed into the function creating the body of the modal.
|
||||||
export interface IModalControl {
|
export interface IModalControl {
|
||||||
@ -466,6 +467,10 @@ export const cssModalButtons = styled('div', `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssFadeIn = keyframes(`
|
||||||
|
from {background-color: transparent}
|
||||||
|
`);
|
||||||
|
|
||||||
const cssModalBacker = styled('div', `
|
const cssModalBacker = styled('div', `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -478,6 +483,8 @@ const cssModalBacker = styled('div', `
|
|||||||
z-index: 999;
|
z-index: 999;
|
||||||
background-color: ${colors.backdrop};
|
background-color: ${colors.backdrop};
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
animation-name: ${cssFadeIn};
|
||||||
|
animation-duration: 0.4s;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssSpinner = styled('div', `
|
const cssSpinner = styled('div', `
|
||||||
|
@ -84,6 +84,14 @@ export function getOrgName(org: Organization): string {
|
|||||||
return org.owner ? `@` + org.owner.name : org.name;
|
return org.owner ? `@` + org.owner.name : org.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given org is the templates org, which contains the public templates.
|
||||||
|
*/
|
||||||
|
export function isTemplatesOrg(org: Organization): boolean {
|
||||||
|
// TODO: It would be nice to have a more robust way to detect the templates org.
|
||||||
|
return org.domain === 'templates' || org.domain === 'templates-s';
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkspaceProperties = CommonProperties;
|
export type WorkspaceProperties = CommonProperties;
|
||||||
export const workspacePropertyKeys = ['createdAt', 'name', 'updatedAt'];
|
export const workspacePropertyKeys = ['createdAt', 'name', 'updatedAt'];
|
||||||
|
|
||||||
|
@ -60,6 +60,7 @@ export const commonUrls = {
|
|||||||
help: "https://support.getgrist.com",
|
help: "https://support.getgrist.com",
|
||||||
plans: "https://www.getgrist.com/pricing",
|
plans: "https://www.getgrist.com/pricing",
|
||||||
createTeamSite: "https://www.getgrist.com/create-team-site",
|
createTeamSite: "https://www.getgrist.com/create-team-site",
|
||||||
|
sproutsProgram: "https://www.getgrist.com/sprouts-program",
|
||||||
|
|
||||||
efcrConnect: 'https://efc-r.com/connect',
|
efcrConnect: 'https://efc-r.com/connect',
|
||||||
efcrHelp: 'https://www.nioxus.info/eFCR-Help',
|
efcrHelp: 'https://www.nioxus.info/eFCR-Help',
|
||||||
|
@ -21,10 +21,10 @@ describe('HomeIntro', function() {
|
|||||||
|
|
||||||
// Check message specific to anon
|
// Check message specific to anon
|
||||||
assert.equal(await driver.find('.test-welcome-title').getText(), 'Welcome to Grist!');
|
assert.equal(await driver.find('.test-welcome-title').getText(), 'Welcome to Grist!');
|
||||||
assert.match(await driver.find('.test-welcome-text').getText(), /without logging in.*need to sign up/);
|
assert.match(await driver.find('.test-welcome-text').getText(), /Sign up.*Visit our Help Center/);
|
||||||
|
|
||||||
// Check the sign-up link.
|
// Check the sign-up link.
|
||||||
const signUp = await driver.findContent('.test-welcome-text a', 'sign up');
|
const signUp = await driver.findContent('.test-welcome-text a', 'Sign up');
|
||||||
assert.include(await signUp.getAttribute('href'), '/signin');
|
assert.include(await signUp.getAttribute('href'), '/signin');
|
||||||
|
|
||||||
// Check that the link takes us to a Grist login page.
|
// Check that the link takes us to a Grist login page.
|
||||||
@ -34,21 +34,7 @@ describe('HomeIntro', function() {
|
|||||||
await gu.waitForDocMenuToLoad();
|
await gu.waitForDocMenuToLoad();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check intro screen.
|
it('should should intro screen for anon', () => testIntroScreen({team: false}));
|
||||||
it('should should intro screen for anon, with video thumbnail', async function() {
|
|
||||||
// Check image for first video.
|
|
||||||
assert.equal(await driver.find('.test-intro-image img').isPresent(), true);
|
|
||||||
await checkImageLoaded(driver.find('.test-intro-image img'));
|
|
||||||
|
|
||||||
// Check links to first video in image and title.
|
|
||||||
assert.include(await driver.find('.test-intro-image img').findClosest('a').getAttribute('href'),
|
|
||||||
'support.getgrist.com');
|
|
||||||
|
|
||||||
// Check link to Help Center
|
|
||||||
assert.include(await driver.findContent('.test-welcome-text a', /Help Center/).getAttribute('href'),
|
|
||||||
'support.getgrist.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show Other Sites section', testOtherSitesSection);
|
it('should not show Other Sites section', testOtherSitesSection);
|
||||||
it('should allow create/import from intro screen', testCreateImport.bind(null, false));
|
it('should allow create/import from intro screen', testCreateImport.bind(null, false));
|
||||||
it('should allow collapsing examples and remember the state', testExamplesCollapsing);
|
it('should allow collapsing examples and remember the state', testExamplesCollapsing);
|
||||||
@ -70,12 +56,12 @@ describe('HomeIntro', function() {
|
|||||||
|
|
||||||
// Check message specific to logged-in user
|
// Check message specific to logged-in user
|
||||||
assert.match(await driver.find('.test-welcome-title').getText(), new RegExp(`Welcome.* ${session.name}`));
|
assert.match(await driver.find('.test-welcome-title').getText(), new RegExp(`Welcome.* ${session.name}`));
|
||||||
assert.match(await driver.find('.test-welcome-text').getText(), /Watch video/);
|
assert.match(await driver.find('.test-welcome-text').getText(), /Visit our Help Center/);
|
||||||
assert.notMatch(await driver.find('.test-welcome-text').getText(), /sign up/);
|
assert.notMatch(await driver.find('.test-welcome-text').getText(), /sign up/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show Other Sites section', testOtherSitesSection);
|
it('should not show Other Sites section', testOtherSitesSection);
|
||||||
it('should show intro screen for empty org', testIntroScreenLoggedIn);
|
it('should show intro screen for empty org', () => testIntroScreen({team: false}));
|
||||||
it('should allow create/import from intro screen', testCreateImport.bind(null, true));
|
it('should allow create/import from intro screen', testCreateImport.bind(null, true));
|
||||||
it('should allow collapsing examples and remember the state', testExamplesCollapsing);
|
it('should allow collapsing examples and remember the state', testExamplesCollapsing);
|
||||||
it('should show examples workspace with the intro', testExamplesSection);
|
it('should show examples workspace with the intro', testExamplesSection);
|
||||||
@ -93,14 +79,15 @@ describe('HomeIntro', function() {
|
|||||||
// Open doc-menu
|
// Open doc-menu
|
||||||
await session.loadDocMenu('/', 'skipWelcomeQuestions');
|
await session.loadDocMenu('/', 'skipWelcomeQuestions');
|
||||||
|
|
||||||
// Check message specific to logged-in user
|
// Check message specific to logged-in user and an empty team site.
|
||||||
assert.match(await driver.find('.test-welcome-title').getText(), new RegExp(`Welcome.* ${session.name}`));
|
assert.match(await driver.find('.test-welcome-title').getText(), new RegExp(`Welcome.* ${session.orgName}`));
|
||||||
assert.match(await driver.find('.test-welcome-text').getText(), /Watch video/);
|
assert.match(await driver.find('.test-welcome-text').getText(), /Learn more.*find an expert/);
|
||||||
assert.notMatch(await driver.find('.test-welcome-text').getText(), /sign up/);
|
assert.notMatch(await driver.find('.test-welcome-text').getText(), /sign up/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show Other Sites section', testOtherSitesSection);
|
it('should not show Other Sites section', testOtherSitesSection);
|
||||||
it('should show intro screen for empty org', testIntroScreenLoggedIn);
|
it('should show intro screen for empty org', () => testIntroScreen({team: true}));
|
||||||
|
it('should allow create/import from intro screen', testCreateImport.bind(null, true));
|
||||||
it('should show examples workspace with the intro', testExamplesSection);
|
it('should show examples workspace with the intro', testExamplesSection);
|
||||||
it('should allow copying examples', testCopyingExamples.bind(null, gu.session().teamSite.orgName));
|
it('should allow copying examples', testCopyingExamples.bind(null, gu.session().teamSite.orgName));
|
||||||
it('should render selected Examples workspace specially', testSelectedExamplesPage);
|
it('should render selected Examples workspace specially', testSelectedExamplesPage);
|
||||||
@ -111,18 +98,23 @@ describe('HomeIntro', function() {
|
|||||||
assert.isFalse(await driver.find('.test-dm-other-sites-header').isPresent());
|
assert.isFalse(await driver.find('.test-dm-other-sites-header').isPresent());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testIntroScreenLoggedIn() {
|
async function testIntroScreen(options: {team: boolean}) {
|
||||||
// Check image for first video.
|
// TODO There is no longer a thumbnail + video link on an empty site, but it's a good place to
|
||||||
assert.equal(await driver.find('.test-intro-image img').isPresent(), true);
|
// check for the presence and functionality of the planned links that open an intro video.
|
||||||
await checkImageLoaded(driver.find('.test-intro-image img'));
|
|
||||||
|
|
||||||
// Check link to first video in welcome text
|
|
||||||
assert.include(await driver.findContent('.test-welcome-text a', /creating a document/).getAttribute('href'),
|
|
||||||
'support.getgrist.com');
|
|
||||||
|
|
||||||
// Check link to Help Center
|
// Check link to Help Center
|
||||||
assert.include(await driver.findContent('.test-welcome-text a', /Help Center/).getAttribute('href'),
|
assert.include(await driver.findContent('.test-welcome-text a', /Help Center/).getAttribute('href'),
|
||||||
'support.getgrist.com');
|
'support.getgrist.com');
|
||||||
|
|
||||||
|
if (options.team) {
|
||||||
|
assert.equal(await driver.find('.test-intro-invite').getText(), 'Invite Team Members');
|
||||||
|
assert.equal(await driver.find('.test-topbar-manage-team').getText(), 'Manage Team');
|
||||||
|
} else {
|
||||||
|
assert.equal(await driver.find('.test-intro-invite').isPresent(), false);
|
||||||
|
assert.equal(await driver.find('.test-topbar-manage-team').isPresent(), false);
|
||||||
|
assert.equal(await driver.find('.test-intro-templates').getText(), 'Browse Templates');
|
||||||
|
assert.include(await driver.find('.test-intro-templates').getAttribute('href'), '/p/templates');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testCreateImport(isLoggedIn: boolean) {
|
async function testCreateImport(isLoggedIn: boolean) {
|
||||||
|
Loading…
Reference in New Issue
Block a user