mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
090d9af21d
commit
6dcdd22792
@ -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')
|
||||
);
|
||||
};
|
||||
|
@ -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.
|
||||
|
@ -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};
|
||||
}
|
||||
`);
|
||||
|
@ -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,
|
||||
|
@ -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> {
|
||||
|
@ -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';
|
||||
|
@ -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'),
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -732,7 +732,7 @@ export class FlexServer implements GristServer {
|
||||
const user = getUser(req);
|
||||
if (user && user.isFirstTimeUser) {
|
||||
log.debug(`welcoming user: ${user.name}`);
|
||||
// Reset isFirstTimeUser flag.
|
||||
// Reset isFirstTimeUser flag.
|
||||
await this._dbManager.updateUser(user.id, {isFirstTimeUser: false});
|
||||
|
||||
// This is a good time to set some other flags, for showing a popup with welcome question(s)
|
||||
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user