(core) Redirect less often in welcomeNewUser

Summary:
Instead of always redirecting new users to the home page or the (teams) welcome page,
only redirect when the user signed in for the first time on a personal site, has access to
other sites, and isn't already being redirected to a specific page on their personal site.

Also tweaks how invalid Choice column values are displayed to match Choice List
columns, and fixes a small CSS issue with select by in the page widget picker when
there are options with long labels.

Test Plan: Browser tests.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3461
This commit is contained in:
George Gevoian 2022-06-06 10:42:51 -07:00
parent 090d9af21d
commit 6dcdd22792
9 changed files with 55 additions and 58 deletions

View File

@ -33,7 +33,6 @@ import {decodeObject} from 'app/plugin/objtypes';
import {isList, isNumberType, isRefListType} from 'app/common/gristTypes';
import {choiceToken} from 'app/client/widgets/ChoiceToken';
import {ChoiceOptions} from 'app/client/widgets/ChoiceTextBox';
import {cssInvalidToken} from 'app/client/widgets/ChoiceListCell';
interface IFilterMenuOptions {
model: ColumnFilterMenuModel;
@ -455,9 +454,9 @@ function getRenderFunc(columnType: string, fieldOrColumn: ViewFieldRec|ColumnRec
fontUnderline: choiceOptions[value.label]?.fontUnderline ?? false,
fontItalic: choiceOptions[value.label]?.fontItalic ?? false,
fontStrikethrough: choiceOptions[value.label]?.fontStrikethrough ?? false,
invalid: !choiceSet.has(value.label),
},
dom.cls(cssToken.className),
cssInvalidToken.cls('-invalid', !choiceSet.has(value.label)),
testId('choice-token')
);
};

View File

@ -342,11 +342,11 @@ export class PageWidgetSelect extends Disposable {
cssFooter(
cssFooterContent(
// If _selectByOptions exists and has more than then "NoLinkOption", show the selector.
dom.maybe((use) => this._selectByOptions && use(this._selectByOptions).length > 1, () => [
dom.maybe((use) => this._selectByOptions && use(this._selectByOptions).length > 1, () => cssSelectBy(
cssSmallLabel('SELECT BY'),
dom.update(cssSelect(this._value.link, this._selectByOptions!),
testId('selectby'))
]),
)),
dom('div', {style: 'flex-grow: 1'}),
bigPrimaryButton(
// TODO: The button's label of the page widget picker should read 'Close' instead when
@ -540,6 +540,12 @@ const cssSmallLabel = styled('span', `
const cssSelect = styled(select, `
flex: 1 0 160px;
width: 160px;
`);
const cssSelectBy = styled('div', `
display: flex;
align-items: center;
`);
// Returns a copy of array with its items sorted in the same order as they appear in other.

View File

@ -1,20 +1,18 @@
import {DataRowModel} from 'app/client/models/DataRowModel';
import {colors, testId} from 'app/client/ui2018/cssVars';
import {testId} from 'app/client/ui2018/cssVars';
import {
ChoiceOptionsByName,
ChoiceTextBox,
} from 'app/client/widgets/ChoiceTextBox';
import {CellValue} from 'app/common/DocActions';
import {decodeObject} from 'app/plugin/objtypes';
import {Computed, dom, styled} from 'grainjs';
import {dom, styled} from 'grainjs';
import {choiceToken} from 'app/client/widgets/ChoiceToken';
/**
* ChoiceListCell - A cell that renders a list of choice tokens.
*/
export class ChoiceListCell extends ChoiceTextBox {
private _choiceSet = Computed.create(this, this.getChoiceValues(), (use, values) => new Set(values));
public buildDom(row: DataRowModel) {
const value = row.cells[this.field.colId.peek()];
@ -25,7 +23,7 @@ export class ChoiceListCell extends ChoiceTextBox {
dom.domComputed((use) => {
return use(row._isAddRow) ? null :
[
use(value), use(this._choiceSet),
use(value), use(this.getChoiceValuesSet()),
use(this.getChoiceOptions())
] as [CellValue, Set<string>, ChoiceOptionsByName];
}, (input) => {
@ -38,8 +36,10 @@ export class ChoiceListCell extends ChoiceTextBox {
return tokens.map(token =>
choiceToken(
String(token),
choiceOptionsByName.get(String(token)) || {},
cssInvalidToken.cls('-invalid', !choiceSet.has(String(token))),
{
...(choiceOptionsByName.get(String(token)) || {}),
invalid: !choiceSet.has(String(token)),
},
dom.cls(cssToken.className),
testId('choice-list-cell-token')
)
@ -69,11 +69,3 @@ export const cssToken = styled('div', `
margin: 2px;
line-height: 16px;
`);
export const cssInvalidToken = styled('div', `
&-invalid {
background-color: white !important;
box-shadow: inset 0 0 0 1px var(--grist-color-error);
color: ${colors.slate};
}
`);

View File

@ -4,7 +4,6 @@ import {IAutocompleteOptions} from 'app/client/lib/autocomplete';
import {IToken, TokenField, tokenFieldStyles} from 'app/client/lib/TokenField';
import {colors, testId} from 'app/client/ui2018/cssVars';
import {menuCssClass} from 'app/client/ui2018/menus';
import {cssInvalidToken} from 'app/client/widgets/ChoiceListCell';
import {createMobileButtons, getButtonMargins} from 'app/client/widgets/EditorButtons';
import {EditorPlacement} from 'app/client/widgets/EditorPlacement';
import {NewBaseEditor, Options} from 'app/client/widgets/NewBaseEditor';
@ -13,7 +12,7 @@ import {CellValue} from "app/common/DocActions";
import {decodeObject, encodeObject} from 'app/plugin/objtypes';
import {dom, styled} from 'grainjs';
import {ChoiceOptions, getRenderFillColor, getRenderTextColor} from 'app/client/widgets/ChoiceTextBox';
import {choiceToken, cssChoiceACItem} from 'app/client/widgets/ChoiceToken';
import {choiceToken, cssChoiceACItem, cssChoiceToken} from 'app/client/widgets/ChoiceToken';
import {icon} from 'app/client/ui2018/icons';
export class ChoiceItem implements ACItem, IToken {
@ -79,7 +78,7 @@ export class ChoiceListEditor extends NewBaseEditor {
dom.cls('font-underline', this._choiceOptionsByName[item.label]?.fontUnderline ?? false),
dom.cls('font-italic', this._choiceOptionsByName[item.label]?.fontItalic ?? false),
dom.cls('font-strikethrough', this._choiceOptionsByName[item.label]?.fontStrikethrough ?? false),
cssInvalidToken.cls('-invalid', item.isInvalid)
cssChoiceToken.cls('-invalid', item.isInvalid)
],
createToken: label => new ChoiceItem(label, !choiceSet.has(label)),
acOptions,

View File

@ -27,6 +27,7 @@ export function getRenderTextColor(choiceOptions?: IChoiceOptions) {
export class ChoiceTextBox extends NTextBox {
private _choices: KoSaveableObservable<string[]>;
private _choiceValues: Computed<string[]>;
private _choiceValuesSet: Computed<Set<string>>;
private _choiceOptions: KoSaveableObservable<ChoiceOptions | null | undefined>;
private _choiceOptionsByName: Computed<ChoiceOptionsByName>
@ -35,6 +36,7 @@ export class ChoiceTextBox extends NTextBox {
this._choices = this.options.prop('choices');
this._choiceOptions = this.options.prop('choiceOptions');
this._choiceValues = Computed.create(this, (use) => use(this._choices) || []);
this._choiceValuesSet = Computed.create(this, this._choiceValues, (_use, values) => new Set(values));
this._choiceOptionsByName = Computed.create(this, (use) => toMap(use(this._choiceOptions)));
}
@ -49,12 +51,14 @@ export class ChoiceTextBox extends NTextBox {
const formattedValue = use(this.valueFormatter).formatAny(use(value));
if (formattedValue === '') { return null; }
const choiceOptions = use(this._choiceOptionsByName).get(formattedValue);
return choiceToken(
formattedValue,
choiceOptions || {},
{
...(use(this._choiceOptionsByName).get(formattedValue) || {}),
invalid: !use(this._choiceValuesSet).has(formattedValue),
},
dom.cls(cssChoiceText.className),
testId('choice-text')
testId('choice-token')
);
}),
),
@ -81,8 +85,8 @@ export class ChoiceTextBox extends NTextBox {
return this.buildConfigDom();
}
protected getChoiceValues(): Computed<string[]> {
return this._choiceValues;
protected getChoiceValuesSet(): Computed<Set<string>> {
return this._choiceValuesSet;
}
protected getChoiceOptions(): Computed<ChoiceOptionsByName> {

View File

@ -5,7 +5,9 @@ import {Style} from 'app/client/models/Styles';
export const DEFAULT_FILL_COLOR = colors.mediumGreyOpaque.value;
export const DEFAULT_TEXT_COLOR = '#000000';
export type IChoiceTokenOptions = Style;
export interface IChoiceTokenOptions extends Style {
invalid?: boolean;
}
/**
* Creates a colored token representing a choice (e.g. Choice and Choice List values).
@ -23,9 +25,11 @@ export type IChoiceTokenOptions = Style;
*/
export function choiceToken(
label: DomElementArg,
{fillColor, textColor, fontBold, fontItalic, fontUnderline, fontStrikethrough}: IChoiceTokenOptions,
options: IChoiceTokenOptions,
...args: DomElementArg[]
): DomContents {
const {fillColor, textColor, fontBold, fontItalic, fontUnderline,
fontStrikethrough, invalid} = options;
return cssChoiceToken(
label,
dom.style('background-color', fillColor ?? DEFAULT_FILL_COLOR),
@ -34,17 +38,23 @@ export function choiceToken(
dom.cls('font-underline', fontUnderline ?? false),
dom.cls('font-italic', fontItalic ?? false),
dom.cls('font-strikethrough', fontStrikethrough ?? false),
invalid ? cssChoiceToken.cls('-invalid') : null,
...args
);
}
const cssChoiceToken = styled('div', `
export const cssChoiceToken = styled('div', `
display: inline-block;
padding: 1px 4px;
border-radius: 3px;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
&-invalid {
background-color: white !important;
box-shadow: inset 0 0 0 1px ${colors.error};
}
`);
const ADD_NEW_HEIGHT = '37px';

View File

@ -5,7 +5,7 @@ import { IToken, TokenField, tokenFieldStyles } from 'app/client/lib/TokenField'
import { reportError } from 'app/client/models/errors';
import { colors, testId } from 'app/client/ui2018/cssVars';
import { menuCssClass } from 'app/client/ui2018/menus';
import { cssInvalidToken } from 'app/client/widgets/ChoiceListCell';
import { cssChoiceToken } from 'app/client/widgets/ChoiceToken';
import { createMobileButtons, getButtonMargins } from 'app/client/widgets/EditorButtons';
import { EditorPlacement } from 'app/client/widgets/EditorPlacement';
import { NewBaseEditor, Options } from 'app/client/widgets/NewBaseEditor';
@ -87,7 +87,7 @@ export class ReferenceListEditor extends NewBaseEditor {
return [
isBlankReference ? '[Blank]' : item.text,
cssToken.cls('-blank', isBlankReference),
cssInvalidToken.cls('-invalid', item.rowId === 'invalid')
cssChoiceToken.cls('-invalid', item.rowId === 'invalid')
];
},
createToken: text => new ReferenceItem(text, 'invalid'),

View File

@ -200,19 +200,7 @@ const rightType: {[key in GristType]: (value: CellValue) => boolean} = {
ManualSortPos: isNumber,
Ref: isNumber,
RefList: isListOrNull,
Choice: (v: CellValue, options?: any) => {
// TODO widgets options should not be used outside of the client. They are an instance of
// modelUtil.jsonObservable, passed in by FieldBuilder.
if (v === '') {
// Accept empty-string values as valid
return true;
} else if (options) {
const choices = options().choices;
return Array.isArray(choices) && choices.includes(v);
} else {
return false;
}
},
Choice: isString,
ChoiceList: isListOrNull,
};

View File

@ -744,19 +744,18 @@ export class FlexServer implements GristServer {
recordSignUpEvent: true,
}});
if (process.env.GRIST_SINGLE_ORG) {
// Merged org is not meaningful in this case.
return res.redirect(this.getHomeUrl(req));
const domain = mreq.org ?? null;
if (!process.env.GRIST_SINGLE_ORG && this._dbManager.isMergedOrg(domain)) {
// We're logging in for the first time on the merged org; if the user has
// access to other team sites, forward the user to a page that lists all
// the teams they have access to.
const result = await this._dbManager.getMergedOrgs(user.id, user.id, domain);
const orgs = this._dbManager.unwrapQueryResult(result);
if (orgs.length > 1 && mreq.path === '/') {
// Only forward if the request is for the home page.
return res.redirect(this.getMergedOrgUrl(mreq, '/welcome/teams'));
}
}
// Redirect to teams page if users has access to more than one org. Otherwise, redirect to
// personal org.
const domain = mreq.org;
const result = await this._dbManager.getMergedOrgs(user.id, user.id, domain || null);
const orgs = (result.status === 200) ? result.data : null;
const redirectPath = orgs && orgs.length > 1 ? '/welcome/teams' : '/';
const redirectUrl = this.getMergedOrgUrl(mreq, redirectPath);
return res.redirect(redirectUrl);
}
if (mreq.org && mreq.org.startsWith('o-')) {
// We are on a team site without a custom subdomain.