mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +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