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 {isList, isNumberType, isRefListType} from 'app/common/gristTypes';
|
||||||
import {choiceToken} from 'app/client/widgets/ChoiceToken';
|
import {choiceToken} from 'app/client/widgets/ChoiceToken';
|
||||||
import {ChoiceOptions} from 'app/client/widgets/ChoiceTextBox';
|
import {ChoiceOptions} from 'app/client/widgets/ChoiceTextBox';
|
||||||
import {cssInvalidToken} from 'app/client/widgets/ChoiceListCell';
|
|
||||||
|
|
||||||
interface IFilterMenuOptions {
|
interface IFilterMenuOptions {
|
||||||
model: ColumnFilterMenuModel;
|
model: ColumnFilterMenuModel;
|
||||||
@ -455,9 +454,9 @@ function getRenderFunc(columnType: string, fieldOrColumn: ViewFieldRec|ColumnRec
|
|||||||
fontUnderline: choiceOptions[value.label]?.fontUnderline ?? false,
|
fontUnderline: choiceOptions[value.label]?.fontUnderline ?? false,
|
||||||
fontItalic: choiceOptions[value.label]?.fontItalic ?? false,
|
fontItalic: choiceOptions[value.label]?.fontItalic ?? false,
|
||||||
fontStrikethrough: choiceOptions[value.label]?.fontStrikethrough ?? false,
|
fontStrikethrough: choiceOptions[value.label]?.fontStrikethrough ?? false,
|
||||||
|
invalid: !choiceSet.has(value.label),
|
||||||
},
|
},
|
||||||
dom.cls(cssToken.className),
|
dom.cls(cssToken.className),
|
||||||
cssInvalidToken.cls('-invalid', !choiceSet.has(value.label)),
|
|
||||||
testId('choice-token')
|
testId('choice-token')
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -342,11 +342,11 @@ export class PageWidgetSelect extends Disposable {
|
|||||||
cssFooter(
|
cssFooter(
|
||||||
cssFooterContent(
|
cssFooterContent(
|
||||||
// If _selectByOptions exists and has more than then "NoLinkOption", show the selector.
|
// 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'),
|
cssSmallLabel('SELECT BY'),
|
||||||
dom.update(cssSelect(this._value.link, this._selectByOptions!),
|
dom.update(cssSelect(this._value.link, this._selectByOptions!),
|
||||||
testId('selectby'))
|
testId('selectby'))
|
||||||
]),
|
)),
|
||||||
dom('div', {style: 'flex-grow: 1'}),
|
dom('div', {style: 'flex-grow: 1'}),
|
||||||
bigPrimaryButton(
|
bigPrimaryButton(
|
||||||
// TODO: The button's label of the page widget picker should read 'Close' instead when
|
// 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, `
|
const cssSelect = styled(select, `
|
||||||
flex: 1 0 160px;
|
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.
|
// 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 {DataRowModel} from 'app/client/models/DataRowModel';
|
||||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
import {testId} from 'app/client/ui2018/cssVars';
|
||||||
import {
|
import {
|
||||||
ChoiceOptionsByName,
|
ChoiceOptionsByName,
|
||||||
ChoiceTextBox,
|
ChoiceTextBox,
|
||||||
} from 'app/client/widgets/ChoiceTextBox';
|
} from 'app/client/widgets/ChoiceTextBox';
|
||||||
import {CellValue} from 'app/common/DocActions';
|
import {CellValue} from 'app/common/DocActions';
|
||||||
import {decodeObject} from 'app/plugin/objtypes';
|
import {decodeObject} from 'app/plugin/objtypes';
|
||||||
import {Computed, dom, styled} from 'grainjs';
|
import {dom, styled} from 'grainjs';
|
||||||
import {choiceToken} from 'app/client/widgets/ChoiceToken';
|
import {choiceToken} from 'app/client/widgets/ChoiceToken';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ChoiceListCell - A cell that renders a list of choice tokens.
|
* ChoiceListCell - A cell that renders a list of choice tokens.
|
||||||
*/
|
*/
|
||||||
export class ChoiceListCell extends ChoiceTextBox {
|
export class ChoiceListCell extends ChoiceTextBox {
|
||||||
private _choiceSet = Computed.create(this, this.getChoiceValues(), (use, values) => new Set(values));
|
|
||||||
|
|
||||||
public buildDom(row: DataRowModel) {
|
public buildDom(row: DataRowModel) {
|
||||||
const value = row.cells[this.field.colId.peek()];
|
const value = row.cells[this.field.colId.peek()];
|
||||||
|
|
||||||
@ -25,7 +23,7 @@ export class ChoiceListCell extends ChoiceTextBox {
|
|||||||
dom.domComputed((use) => {
|
dom.domComputed((use) => {
|
||||||
return use(row._isAddRow) ? null :
|
return use(row._isAddRow) ? null :
|
||||||
[
|
[
|
||||||
use(value), use(this._choiceSet),
|
use(value), use(this.getChoiceValuesSet()),
|
||||||
use(this.getChoiceOptions())
|
use(this.getChoiceOptions())
|
||||||
] as [CellValue, Set<string>, ChoiceOptionsByName];
|
] as [CellValue, Set<string>, ChoiceOptionsByName];
|
||||||
}, (input) => {
|
}, (input) => {
|
||||||
@ -38,8 +36,10 @@ export class ChoiceListCell extends ChoiceTextBox {
|
|||||||
return tokens.map(token =>
|
return tokens.map(token =>
|
||||||
choiceToken(
|
choiceToken(
|
||||||
String(token),
|
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),
|
dom.cls(cssToken.className),
|
||||||
testId('choice-list-cell-token')
|
testId('choice-list-cell-token')
|
||||||
)
|
)
|
||||||
@ -69,11 +69,3 @@ export const cssToken = styled('div', `
|
|||||||
margin: 2px;
|
margin: 2px;
|
||||||
line-height: 16px;
|
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 {IToken, TokenField, tokenFieldStyles} from 'app/client/lib/TokenField';
|
||||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||||
import {menuCssClass} from 'app/client/ui2018/menus';
|
import {menuCssClass} from 'app/client/ui2018/menus';
|
||||||
import {cssInvalidToken} from 'app/client/widgets/ChoiceListCell';
|
|
||||||
import {createMobileButtons, getButtonMargins} from 'app/client/widgets/EditorButtons';
|
import {createMobileButtons, getButtonMargins} from 'app/client/widgets/EditorButtons';
|
||||||
import {EditorPlacement} from 'app/client/widgets/EditorPlacement';
|
import {EditorPlacement} from 'app/client/widgets/EditorPlacement';
|
||||||
import {NewBaseEditor, Options} from 'app/client/widgets/NewBaseEditor';
|
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 {decodeObject, encodeObject} from 'app/plugin/objtypes';
|
||||||
import {dom, styled} from 'grainjs';
|
import {dom, styled} from 'grainjs';
|
||||||
import {ChoiceOptions, getRenderFillColor, getRenderTextColor} from 'app/client/widgets/ChoiceTextBox';
|
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';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
|
|
||||||
export class ChoiceItem implements ACItem, IToken {
|
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-underline', this._choiceOptionsByName[item.label]?.fontUnderline ?? false),
|
||||||
dom.cls('font-italic', this._choiceOptionsByName[item.label]?.fontItalic ?? false),
|
dom.cls('font-italic', this._choiceOptionsByName[item.label]?.fontItalic ?? false),
|
||||||
dom.cls('font-strikethrough', this._choiceOptionsByName[item.label]?.fontStrikethrough ?? 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)),
|
createToken: label => new ChoiceItem(label, !choiceSet.has(label)),
|
||||||
acOptions,
|
acOptions,
|
||||||
|
@ -27,6 +27,7 @@ export function getRenderTextColor(choiceOptions?: IChoiceOptions) {
|
|||||||
export class ChoiceTextBox extends NTextBox {
|
export class ChoiceTextBox extends NTextBox {
|
||||||
private _choices: KoSaveableObservable<string[]>;
|
private _choices: KoSaveableObservable<string[]>;
|
||||||
private _choiceValues: Computed<string[]>;
|
private _choiceValues: Computed<string[]>;
|
||||||
|
private _choiceValuesSet: Computed<Set<string>>;
|
||||||
private _choiceOptions: KoSaveableObservable<ChoiceOptions | null | undefined>;
|
private _choiceOptions: KoSaveableObservable<ChoiceOptions | null | undefined>;
|
||||||
private _choiceOptionsByName: Computed<ChoiceOptionsByName>
|
private _choiceOptionsByName: Computed<ChoiceOptionsByName>
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ export class ChoiceTextBox extends NTextBox {
|
|||||||
this._choices = this.options.prop('choices');
|
this._choices = this.options.prop('choices');
|
||||||
this._choiceOptions = this.options.prop('choiceOptions');
|
this._choiceOptions = this.options.prop('choiceOptions');
|
||||||
this._choiceValues = Computed.create(this, (use) => use(this._choices) || []);
|
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)));
|
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));
|
const formattedValue = use(this.valueFormatter).formatAny(use(value));
|
||||||
if (formattedValue === '') { return null; }
|
if (formattedValue === '') { return null; }
|
||||||
|
|
||||||
const choiceOptions = use(this._choiceOptionsByName).get(formattedValue);
|
|
||||||
return choiceToken(
|
return choiceToken(
|
||||||
formattedValue,
|
formattedValue,
|
||||||
choiceOptions || {},
|
{
|
||||||
|
...(use(this._choiceOptionsByName).get(formattedValue) || {}),
|
||||||
|
invalid: !use(this._choiceValuesSet).has(formattedValue),
|
||||||
|
},
|
||||||
dom.cls(cssChoiceText.className),
|
dom.cls(cssChoiceText.className),
|
||||||
testId('choice-text')
|
testId('choice-token')
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@ -81,8 +85,8 @@ export class ChoiceTextBox extends NTextBox {
|
|||||||
return this.buildConfigDom();
|
return this.buildConfigDom();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getChoiceValues(): Computed<string[]> {
|
protected getChoiceValuesSet(): Computed<Set<string>> {
|
||||||
return this._choiceValues;
|
return this._choiceValuesSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getChoiceOptions(): Computed<ChoiceOptionsByName> {
|
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_FILL_COLOR = colors.mediumGreyOpaque.value;
|
||||||
export const DEFAULT_TEXT_COLOR = '#000000';
|
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).
|
* 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(
|
export function choiceToken(
|
||||||
label: DomElementArg,
|
label: DomElementArg,
|
||||||
{fillColor, textColor, fontBold, fontItalic, fontUnderline, fontStrikethrough}: IChoiceTokenOptions,
|
options: IChoiceTokenOptions,
|
||||||
...args: DomElementArg[]
|
...args: DomElementArg[]
|
||||||
): DomContents {
|
): DomContents {
|
||||||
|
const {fillColor, textColor, fontBold, fontItalic, fontUnderline,
|
||||||
|
fontStrikethrough, invalid} = options;
|
||||||
return cssChoiceToken(
|
return cssChoiceToken(
|
||||||
label,
|
label,
|
||||||
dom.style('background-color', fillColor ?? DEFAULT_FILL_COLOR),
|
dom.style('background-color', fillColor ?? DEFAULT_FILL_COLOR),
|
||||||
@ -34,17 +38,23 @@ export function choiceToken(
|
|||||||
dom.cls('font-underline', fontUnderline ?? false),
|
dom.cls('font-underline', fontUnderline ?? false),
|
||||||
dom.cls('font-italic', fontItalic ?? false),
|
dom.cls('font-italic', fontItalic ?? false),
|
||||||
dom.cls('font-strikethrough', fontStrikethrough ?? false),
|
dom.cls('font-strikethrough', fontStrikethrough ?? false),
|
||||||
|
invalid ? cssChoiceToken.cls('-invalid') : null,
|
||||||
...args
|
...args
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssChoiceToken = styled('div', `
|
export const cssChoiceToken = styled('div', `
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 1px 4px;
|
padding: 1px 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
|
||||||
|
&-invalid {
|
||||||
|
background-color: white !important;
|
||||||
|
box-shadow: inset 0 0 0 1px ${colors.error};
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const ADD_NEW_HEIGHT = '37px';
|
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 { reportError } from 'app/client/models/errors';
|
||||||
import { colors, testId } from 'app/client/ui2018/cssVars';
|
import { colors, testId } from 'app/client/ui2018/cssVars';
|
||||||
import { menuCssClass } from 'app/client/ui2018/menus';
|
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 { createMobileButtons, getButtonMargins } from 'app/client/widgets/EditorButtons';
|
||||||
import { EditorPlacement } from 'app/client/widgets/EditorPlacement';
|
import { EditorPlacement } from 'app/client/widgets/EditorPlacement';
|
||||||
import { NewBaseEditor, Options } from 'app/client/widgets/NewBaseEditor';
|
import { NewBaseEditor, Options } from 'app/client/widgets/NewBaseEditor';
|
||||||
@ -87,7 +87,7 @@ export class ReferenceListEditor extends NewBaseEditor {
|
|||||||
return [
|
return [
|
||||||
isBlankReference ? '[Blank]' : item.text,
|
isBlankReference ? '[Blank]' : item.text,
|
||||||
cssToken.cls('-blank', isBlankReference),
|
cssToken.cls('-blank', isBlankReference),
|
||||||
cssInvalidToken.cls('-invalid', item.rowId === 'invalid')
|
cssChoiceToken.cls('-invalid', item.rowId === 'invalid')
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
createToken: text => new ReferenceItem(text, 'invalid'),
|
createToken: text => new ReferenceItem(text, 'invalid'),
|
||||||
|
@ -200,19 +200,7 @@ const rightType: {[key in GristType]: (value: CellValue) => boolean} = {
|
|||||||
ManualSortPos: isNumber,
|
ManualSortPos: isNumber,
|
||||||
Ref: isNumber,
|
Ref: isNumber,
|
||||||
RefList: isListOrNull,
|
RefList: isListOrNull,
|
||||||
Choice: (v: CellValue, options?: any) => {
|
Choice: isString,
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ChoiceList: isListOrNull,
|
ChoiceList: isListOrNull,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -744,19 +744,18 @@ export class FlexServer implements GristServer {
|
|||||||
recordSignUpEvent: true,
|
recordSignUpEvent: true,
|
||||||
}});
|
}});
|
||||||
|
|
||||||
if (process.env.GRIST_SINGLE_ORG) {
|
const domain = mreq.org ?? null;
|
||||||
// Merged org is not meaningful in this case.
|
if (!process.env.GRIST_SINGLE_ORG && this._dbManager.isMergedOrg(domain)) {
|
||||||
return res.redirect(this.getHomeUrl(req));
|
// 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-')) {
|
if (mreq.org && mreq.org.startsWith('o-')) {
|
||||||
// We are on a team site without a custom subdomain.
|
// We are on a team site without a custom subdomain.
|
||||||
|
Loading…
Reference in New Issue
Block a user