(core) updates from grist-core

pull/391/head
Paul Fitzpatrick 1 year ago
commit cd72a54bbb

@ -0,0 +1,11 @@
# Exclude everything, and use an allowlist to include just the folders we want checked.
/*
!/app
!/test
!/plugins
!/buildtools
!/stubs
!/.eslintrc.js
# And exclude some things within those (generated files)
/plugins/**/dist

@ -0,0 +1,122 @@
module.exports = {
// Basic settings for JS files.
extends: ['eslint:recommended'],
env: {
node: true,
es6: true,
},
// Set parser to support, e.g. import() function for dynamic imports (see
// https://stackoverflow.com/a/47833471/328565 and https://stackoverflow.com/a/69557309/328565).
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2018,
requireConfigFile: false,
},
ignorePatterns: [
"/static/**/*.js", // Built JS bundles
"*-ti.ts", // Files generated by ts-interface-builder
],
rules: {
'no-unused-vars': ["error", {args: "none"}],
'no-prototype-builtins': 'off',
'no-trailing-spaces': 'warn',
'comma-spacing': 'warn',
'semi-spacing': 'warn',
},
// The Typescript-specific settings apply only to TS files. ESLint is expensive here, and has to
// analyze dependencies for type-checking. (In an editor, it's much faster when used with
// tsserver.)
overrides: [{
files: "**/*.ts",
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.eslint.json'],
sourceType: 'module',
ecmaVersion: 2018,
},
env: {
node: true,
browser: true,
es6: true,
mocha: true,
},
globals: {
Promise: true
},
rules: {
// A lot of the options below would be helpful to keep on, but would need a lot of fix-ups.
"@typescript-eslint/ban-types": 'off',
"@typescript-eslint/explicit-member-accessibility": ["error", {overrides: {constructors: 'off'}}],
"@typescript-eslint/explicit-module-boundary-types": 'off',
// These settings mimic what we had before with tslint.
"@typescript-eslint/member-ordering": ["warn", {default: [
'public-static-field',
'public-static-method',
'protected-static-field', 'private-static-field', 'static-field',
'protected-static-method', 'private-static-method', 'static-method',
'public-field', 'protected-field', 'private-field', 'field',
'public-constructor', 'protected-constructor', 'private-constructor', 'constructor',
'public-method', 'protected-method', 'private-method', 'method',
]}],
"@typescript-eslint/naming-convention": ["warn", {
selector: "memberLike", filter: { match: false, regex: '(listenTo)' },
modifiers: ["private"], format: ["camelCase"], leadingUnderscore: "require"
}],
"@typescript-eslint/no-empty-function": 'off',
"@typescript-eslint/no-explicit-any": 'off',
"@typescript-eslint/no-inferrable-types": 'off',
"@typescript-eslint/no-misused-promises": ["error", {"checksVoidReturn": false}],
"@typescript-eslint/no-namespace": 'off',
"@typescript-eslint/no-non-null-assertion": 'off',
"@typescript-eslint/no-shadow": ["warn", { ignoreTypeValueShadow: true }],
"@typescript-eslint/no-this-alias": 'off',
"@typescript-eslint/no-type-alias": ["warn", {
"allowAliases": "always",
"allowCallbacks": "always",
"allowConditionalTypes": "always",
"allowConstructors": "always",
"allowLiterals": "in-unions-and-intersections",
"allowMappedTypes": "always",
"allowTupleTypes": "always",
"allowGenerics": "always",
}],
"@typescript-eslint/no-unsafe-assignment": 'off',
"@typescript-eslint/no-unsafe-argument": 'off',
"@typescript-eslint/no-unsafe-call": 'off',
"@typescript-eslint/no-unsafe-member-access": 'off',
"@typescript-eslint/no-unsafe-return": 'off',
"@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false }],
"@typescript-eslint/no-var-requires": 'off',
"@typescript-eslint/prefer-regexp-exec": 'off',
"@typescript-eslint/require-await": 'off',
"@typescript-eslint/restrict-plus-operands": 'off',
"@typescript-eslint/restrict-template-expressions": 'off',
"@typescript-eslint/type-annotation-spacing": 'warn',
"@typescript-eslint/unbound-method": 'off',
'no-undef': 'off',
'no-prototype-builtins': 'off',
'prefer-rest-params': 'off',
'no-console': 'off',
'no-shadow': 'off',
'no-inner-declarations': 'off',
'max-len': ['warn', {code: 120, ignoreUrls: true}],
'sort-imports': ['warn', {ignoreDeclarationSort: true, ignoreCase: true, allowSeparatedGroups: true}],
'no-trailing-spaces': 'warn',
'no-unused-expressions': ["error", {allowShortCircuit: true, allowTernary: true}],
'block-spacing': ['warn', 'always'],
'comma-spacing': 'warn',
'curly': ['warn', 'all'],
'semi': ['warn', 'always'],
'semi-spacing': 'warn',
},
}]
}

@ -37,6 +37,9 @@ jobs:
- name: Install Node.js packages
run: yarn install
- name: Run eslint
run: yarn run lint:ci
- name: Make sure bucket is versioned
env:
AWS_ACCESS_KEY_ID: administrator
@ -46,6 +49,7 @@ jobs:
- name: Build Node.js code
run: yarn run build:prod
- name: Run smoke test
run: VERBOSE=1 DEBUG=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:smoke

@ -134,7 +134,7 @@ export class AccessRules extends Disposable {
// there were only removals, then length will be reduced.
getChangedStatus(tableRules.length < this._ruleCollection.getAllTableIds().length),
getChangedStatus(userAttr.length < this._ruleCollection.getUserAttributeRules().size),
...tableRules.map(t => use(t.ruleStatus)),
...tableRules.map(tr => use(tr.ruleStatus)),
...userAttr.map(u => use(u.ruleStatus)),
specialRules ? use(specialRules.ruleStatus) : RuleStatus.Unchanged,
);
@ -239,7 +239,7 @@ export class AccessRules extends Disposable {
const newResources: MetaRowRecord<'_grist_ACLResources'>[] = flatten(
[{tableId: '*', colIds: '*'}],
this._specialRules.get()?.getResources() || [],
...this._tableRules.get().map(t => t.getResources()))
...this._tableRules.get().map(tr => tr.getResources()))
.map(r => ({id: -1, ...r}));
// Prepare userActions and a mapping of serializedResource to rowIds.
@ -350,7 +350,7 @@ export class AccessRules extends Disposable {
// synchronously, which prevents the menu from closing on click.
menuItemAsync(() => this._addTableRules(tableId),
this.getTableTitle(tableId),
dom.cls('disabled', (use) => use(this._tableRules).some(t => t.tableId === tableId)),
dom.cls('disabled', (use) => use(this._tableRules).some(tr => tr.tableId === tableId)),
)
),
),
@ -442,7 +442,7 @@ export class AccessRules extends Disposable {
*/
public getRules(): RuleRec[] {
return flatten(
...this._tableRules.get().map(t => t.getRules()),
...this._tableRules.get().map(tr => tr.getRules()),
this._specialRules.get()?.getRules() || [],
this._docDefaultRuleSet.get()?.getRules('*') || []
);
@ -484,7 +484,7 @@ export class AccessRules extends Disposable {
}
private _addTableRules(tableId: string) {
if (this._tableRules.get().some(t => t.tableId === tableId)) {
if (this._tableRules.get().some(tr => tr.tableId === tableId)) {
throw new Error(`Trying to add TableRules for existing table ${tableId}`);
}
const defRuleSet: RuleSet = {tableId, colIds: '*', body: []};

@ -395,7 +395,10 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
const newName = tableRename[1];
if (!newName) {
// TODO - find a better way to send informative notifications.
gristNotify(t("Table {{tableId}} was subsequently removed in action #{{actionNum}}", {tableId:tableId, actionNum: action.actionNum}));
gristNotify(t(
"Table {{tableId}} was subsequently removed in action #{{actionNum}}",
{tableId:tableId, actionNum: action.actionNum}
));
return;
}
tableId = newName;
@ -416,7 +419,10 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
const newName = columnRename[1];
if (!newName) {
// TODO - find a better way to send informative notifications.
gristNotify(t("Column {{colId}} was subsequently removed in action #{{action.actionNum}}", {colId, actionNum: action.actionNum}));
gristNotify(t(
"Column {{colId}} was subsequently removed in action #{{action.actionNum}}",
{colId, actionNum: action.actionNum}
));
return;
}
colId = newName;

@ -656,8 +656,9 @@ export class ChartConfig extends GrainJSDisposable {
),
dom.domComputed(this._optionsObj.prop('errorBars'), (value: ChartOptions["errorBars"]) =>
value === 'symmetric' ? cssRowHelp(t("Each Y series is followed by a series for the length of error bars.")) :
value === 'separate' ? cssRowHelp(t("Each Y series is followed by two series, for top and bottom error bars.")) :
null
value === 'separate' ? cssRowHelp(t("Each Y series is followed by two series, for " +
"top and bottom error bars."))
: null
),
]),

@ -160,7 +160,10 @@ export class DataTables extends Disposable {
function doRemove() {
return docModel.docData.sendAction(['RemoveTable', r.tableId()]);
}
confirmModal(t("Delete {{formattedTableName}} data, and remove it from all pages?", {formattedTableName : r.formattedTableName()}), 'Delete', doRemove);
confirmModal(t(
"Delete {{formattedTableName}} data, and remove it from all pages?",
{formattedTableName : r.formattedTableName()}
), 'Delete', doRemove);
}
private _tableRows(table: TableRec) {

@ -149,7 +149,9 @@ export class DocumentUsage extends Disposable {
return dom.domComputed((use) => {
const isAccessDenied = use(this._isAccessDenied);
if (isAccessDenied === null) { return null; }
if (isAccessDenied) { return buildMessage(t("Usage statistics are only available to users with full access to the document data.")); }
if (isAccessDenied) {
return buildMessage(t("Usage statistics are only available to users with full access to the document data."));
}
const org = use(this._currentOrg);
const product = use(this._currentProduct);
@ -239,7 +241,7 @@ export function buildUpgradeMessage(
) {
if (!canUpgrade) { return t("Contact the site owner to upgrade the plan to raise limits."); }
const upgradeLinkText = t("start your 30-day free trial of the Pro plan.")
const upgradeLinkText = t("start your 30-day free trial of the Pro plan.");
// TODO i18next
return [
variant === 'short' ? null : t("For higher limits, "),

@ -986,7 +986,7 @@ export class GristDoc extends DisposableWithEvents {
* Renames table. Method exposed primarily for tests.
*/
public async renameTable(tableId: string, newTableName: string) {
const tableRec = this.docModel.visibleTables.all().find(t => t.tableId.peek() === tableId);
const tableRec = this.docModel.visibleTables.all().find(tb => tb.tableId.peek() === tableId);
if (!tableRec) {
throw new UserError(`No table with id ${tableId}`);
}

@ -380,7 +380,7 @@ export class Importer extends DisposableWithEvents {
}
private _getHiddenTableIds(): string[] {
return this._sourceInfoArray.get().map((t: SourceInfo) => t.hiddenTableId);
return this._sourceInfoArray.get().map((si: SourceInfo) => si.hiddenTableId);
}
private async _reImport(upload: UploadResult) {

@ -1,11 +1,12 @@
import {computed, Computed, dom, DomElementArg, IDisposableOwner, Observable, styled} from "grainjs";
import {cssModalBody, cssModalButtons, cssModalTitle, IModalControl, modal, cssAnimatedModal} from 'app/client/ui2018/modals';
import {cssAnimatedModal, cssModalBody, cssModalButtons, cssModalTitle,
IModalControl, modal} from 'app/client/ui2018/modals';
import {bigBasicButton, bigPrimaryButton} from 'app/client/ui2018/buttons';
import {mediaXSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
import {UserManagerModel, IOrgMemberSelectOption} from 'app/client/models/UserManagerModel';
import {IOrgMemberSelectOption, UserManagerModel} from 'app/client/models/UserManagerModel';
import {icon} from 'app/client/ui2018/icons';
import {textarea} from "app/client/ui/inputs";
import {BasicRole, VIEWER, NonGuestRole, isBasicRole} from "app/common/roles";
import {BasicRole, isBasicRole, NonGuestRole, VIEWER} from "app/common/roles";
import {menu, menuItem} from 'app/client/ui2018/menus';
function parseEmailList(emailListRaw: string): Array<string> {
@ -28,9 +29,11 @@ export function buildMultiUserManagerModal(
const emailListObs = Observable.create(owner, "");
const rolesObs = Observable.create<BasicRole>(owner, VIEWER);
const isValidObs = Observable.create(owner, true);
const enableAdd: Computed<boolean> = computed((use) => Boolean(use(emailListObs) && use(rolesObs) && use(isValidObs)));
const enableAdd: Computed<boolean> = computed(
(use) => Boolean(use(emailListObs) && use(rolesObs) && use(isValidObs))
);
const save = (ctl: IModalControl) => {
const emailList = parseEmailList(emailListObs.get());
const role = rolesObs.get();
@ -40,7 +43,7 @@ export function buildMultiUserManagerModal(
emailList.forEach(email => onAdd(email, role));
ctl.close();
}
}
};
return modal(ctl => [
{ style: 'padding: 0;' },
@ -63,7 +66,7 @@ export function buildMultiUserManagerModal(
{ style: 'margin: 32px 64px; display: flex;' },
bigPrimaryButton('Confirm',
dom.boolAttr('disabled', (use) => !use(enableAdd)),
dom.on('click', () => {save(ctl)}),
dom.on('click', () => save(ctl)),
testId('um-confirm')
),
bigBasicButton(

@ -101,7 +101,7 @@ function domT(key: string, args: any, tImpl: typeof i18next.t) {
// If there are any DomElements in args, handle it with missingInterpolationHandler.
const domElements = !args ? [] : Object.entries(args).filter(([_, value]) => isLikeDomContents(value));
if (!args || !domElements.length) {
return tImpl(key, args || undefined) as any;
return tImpl(key, args || undefined);
} else {
// Make a copy of the arguments, and remove any dom elements from it. It will instruct
// i18next library to use `missingInterpolationHandler` handler.
@ -171,5 +171,5 @@ export function makeT(scope: string, instance?: typeof i18next) {
reportError(error);
}
return domT(key, args, scopedResolver!);
}
};
}

@ -239,14 +239,24 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
t("Error accessing document"),
t("Reload"),
async () => window.location.reload(true),
isDocOwner ? t("You can try reloading the document, or using recovery mode. Recovery mode opens the document to be fully accessible to owners, and inaccessible to others. It also disables formulas. [{{error}}]", {error: err.message}) :
isDenied ? t('Sorry, access to this document has been denied. [{{error}}]', {error: err.message}) :
t("Document owners can attempt to recover the document. [{{error}}]", {error: err.message}),
{ hideCancel: true,
extraButtons: (isDocOwner && !isDenied) ? bigBasicButton(t("Enter recovery mode"), dom.on('click', async () => {
await this._api.getDocAPI(this.currentDocId.get()!).recover(true);
window.location.reload(true);
}), testId('modal-recovery-mode')) : null,
isDocOwner
? t("You can try reloading the document, or using recovery mode. " +
"Recovery mode opens the document to be fully accessible to " +
"owners, and inaccessible to others. It also disables " +
"formulas. [{{error}}]", {error: err.message})
: isDenied
? t('Sorry, access to this document has been denied. [{{error}}]', {error: err.message})
: t("Document owners can attempt to recover the document. [{{error}}]", {error: err.message}),
{
hideCancel: true,
extraButtons: !(isDocOwner && !isDenied) ? null : bigBasicButton(
t("Enter recovery mode"),
dom.on('click', async () => {
await this._api.getDocAPI(this.currentDocId.get()!).recover(true);
window.location.reload(true);
}),
testId('modal-recovery-mode')
)
},
);
}

@ -113,7 +113,9 @@ export class AccountPage extends Disposable {
),
css.subHeader(t("Two-factor authentication")),
css.description(
t("Two-factor authentication is an extra layer of security for your Grist account designed to ensure that you're the only person who can access your account, even if someone knows your password.")
t("Two-factor authentication is an extra layer of security for your Grist account " +
"designed to ensure that you're the only person who can access your account, " +
"even if someone knows your password.")
),
dom.create(MFAConfig, user),
),

@ -78,7 +78,8 @@ export class ApiKey extends Disposable {
dom.maybe((use) => !(use(this._apiKey) || this._anonymous), () => [
basicButton(t("Create"), dom.on('click', () => this._onCreate()), testId('create'),
dom.boolAttr('disabled', this._loading)),
description(t("By generating an API key, you will be able to make API calls for your own account."), testId('description')),
description(t("By generating an API key, you will be able to " +
"make API calls for your own account."), testId('description')),
]),
);
}
@ -114,7 +115,8 @@ export class ApiKey extends Disposable {
confirmModal(
t("Remove API Key"), t("Remove"),
() => this._onDelete(),
t("You're about to delete an API key. This will cause all future requests using this API key to be rejected. Do you still want to delete?")
t("You're about to delete an API key. This will cause all future requests " +
"using this API key to be rejected. Do you still want to delete?")
);
}
}

@ -73,7 +73,9 @@ class ColumnPicker extends Disposable {
properValue,
options,
{
defaultLabel: this._column.typeDesc != "any" ? t("Pick a {{columnType}} column", {"columnType": this._column.typeDesc}) : t("Pick a column")
defaultLabel: this._column.typeDesc != "any"
? t("Pick a {{columnType}} column", {"columnType": this._column.typeDesc})
: t("Pick a column")
}
),
testId('mapping-for-' + this._column.name),
@ -117,7 +119,11 @@ class ColumnListPicker extends Disposable {
col.label.peek(),
)),
wrongTypeCount > 0 ? menuText(
t("{{wrongTypeCount}} non-{{columnType}} columns are not shown", {wrongTypeCount, columnType: this._column.type.toLowerCase(), count: wrongTypeCount}),
t("{{wrongTypeCount}} non-{{columnType}} columns are not shown", {
wrongTypeCount,
columnType: this._column.type.toLowerCase(),
count: wrongTypeCount
}),
testId('map-message-' + this._column.name)
) : null
];
@ -371,8 +377,12 @@ export class CustomSectionConfig extends Disposable {
}
switch(level) {
case AccessLevel.none: return cssConfirmLine(t("Widget does not require any permissions."));
case AccessLevel.read_table: return cssConfirmLine(t("Widget needs to {{read}} the current table.", {read: dom("b", "read")})); // TODO i18next
case AccessLevel.full: return cssConfirmLine(t("Widget needs {{fullAccess}} to this document.", {fullAccess: dom("b", "full access")})); // TODO i18next
case AccessLevel.read_table:
return cssConfirmLine(t("Widget needs to {{read}} the current table.", {read: dom("b", "read")}));
case AccessLevel.full:
return cssConfirmLine(t("Widget needs {{fullAccess}} to this document.", {
fullAccess: dom("b", "full access")
}));
default: throw new Error(`Unsupported ${level} access level`);
}
}

@ -21,7 +21,8 @@ export async function startDocTour(docData: DocData, docComm: DocComm, onFinishC
const invalidDocTour: IOnBoardingMsg[] = [{
title: t("No valid document tour"),
body: t("Cannot construct a document tour from the data in this document. Ensure there is a table named GristDocTour with columns Title, Body, Placement, and Location."),
body: t("Cannot construct a document tour from the data in this document. " +
"Ensure there is a table named GristDocTour with columns Title, Body, Placement, and Location."),
selector: 'document',
showHasModal: true,
}];

@ -58,7 +58,7 @@ export async function showDocSettingsModal(docInfo: DocInfoRec, docPageModel: Do
canChangeEngine ? [
// Small easter egg: you can click on the skull-and-crossbones to
// force a reload of the document.
cssDataRow(t("Engine (experimental {{span}} change at own risk):", {span:
cssDataRow(t("Engine (experimental {{span}} change at own risk):", {span:
dom('span', '☠',
dom.style('cursor', 'pointer'),
dom.on('click', async () => {

@ -83,10 +83,9 @@ class DuplicateTableModal extends Disposable {
),
cssWarning(
cssWarningIcon('Warning'),
dom('div',
t("Instead of duplicating tables, it's usually better to segment data using linked views. {{link}}", {link: cssLink({href: commonUrls.helpLinkingWidgets, target: '_blank'}, 'Read More.')})
), //TODO: i18next
dom('div', t("Instead of duplicating tables, it's usually better to segment data using linked views. {{link}}",
{link: cssLink({href: commonUrls.helpLinkingWidgets, target: '_blank'}, 'Read More.')}
)),
),
cssField(
cssCheckbox(

@ -1,4 +1,4 @@
import {makeT} from 'app/client/lib/localization'
import {makeT} from 'app/client/lib/localization';
const t = makeT('ExampleInfo');
@ -36,7 +36,8 @@ export const buildExamples = (): IExampleInfo[] => [{
tutorialUrl: 'https://support.getgrist.com/investment-research/',
welcomeCard: {
title: t("Welcome to the Investment Research template"),
text: t("Check out our related tutorial to learn how to create summary tables and charts, and to link charts dynamically."),
text: t("Check out our related tutorial to learn how to create " +
"summary tables and charts, and to link charts dynamically."),
tutorialName: t("Tutorial: Analyze & Visualize"),
},
}, {

@ -41,7 +41,8 @@ export async function replaceTrunkWithFork(user: FullUser|null, doc: Document, a
if (cmp.summary === 'left' || cmp.summary === 'both') {
titleText = t("Original Has Modifications");
buttonText = t("Overwrite");
warningText = `${warningText} ${t("Be careful, the original has changes not in this document. Those changes will be overwritten.")}`;
warningText = `${warningText} ${t("Be careful, the original has changes " +
"not in this document. Those changes will be overwritten.")}`;
} else if (cmp.summary === 'unrelated') {
titleText = t("Original Looks Unrelated");
buttonText = t("Overwrite");

@ -268,8 +268,8 @@ export class PageWidgetSelect extends Disposable {
}) :
null;
private _isNewTableDisabled = Computed.create(this, this._value.type, (use, t) => !isValidSelection(
'New Table', t, this._options.isNewPage));
private _isNewTableDisabled = Computed.create(this, this._value.type, (use, type) => !isValidSelection(
'New Table', type, this._options.isNewPage));
constructor(
private _value: IWidgetValueObs,
@ -399,8 +399,8 @@ export class PageWidgetSelect extends Disposable {
this._value.summarize.set(true);
}
private _selectType(t: IWidgetType) {
this._value.type.set(t);
private _selectType(type: IWidgetType) {
this._value.type.set(type);
}
private _selectTable(tid: TableId) {

@ -100,11 +100,11 @@ function removeView(activeDoc: GristDoc, viewId: number, pageName: string) {
const removePage = () => [['RemoveRecord', '_grist_Views', viewId]];
const removeAll = () => [
...removePage(),
...notVisibleTables.map(t => ['RemoveTable', t.tableId.peek()])
...notVisibleTables.map(tb => ['RemoveTable', tb.tableId.peek()])
];
if (notVisibleTables.length) {
const tableNames = notVisibleTables.map(t => t.tableNameDef.peek());
const tableNames = notVisibleTables.map(tb => tb.tableNameDef.peek());
buildPrompt(tableNames, async (option) => {
// Errors are handled in the dialog.
if (option === 'data') {
@ -174,7 +174,7 @@ function buildOption(value: Observable<RemoveOption>, id: RemoveOption, content:
function buildWarning(tables: string[]) {
return cssWarning(
dom.forEach(tables, (t) => cssTableName(t, testId('table')))
dom.forEach(tables, (tb) => cssTableName(tb, testId('table')))
);
}

@ -39,8 +39,8 @@ import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {loadingSpinner} from 'app/client/ui2018/loaders';
import {menu, menuItem, menuText} from 'app/client/ui2018/menus';
import {confirmModal, cssModalBody, cssModalButtons, cssModalTitle, IModalControl,
modal, cssAnimatedModal} from 'app/client/ui2018/modals';
import {confirmModal, cssAnimatedModal, cssModalBody, cssModalButtons, cssModalTitle,
IModalControl, modal} from 'app/client/ui2018/modals';
export interface IUserManagerOptions {
permissionData: Promise<PermissionData>;

@ -35,7 +35,8 @@ export function createForbiddenPage(appModel: AppModel, message?: string) {
return pagePanelsError(appModel, t("Access denied{{suffix}}", {suffix: ''}), [
dom.domComputed(appModel.currentValidUser, user => user ? [
cssErrorText(message || t("You do not have access to this organization's documents.")),
cssErrorText(t("You are signed in as {{email}}. You can sign in with a different account, or ask an administrator for access.", {email: dom('b', user.email)})), // TODO: i18next
cssErrorText(t("You are signed in as {{email}}. You can sign in with a different " +
"account, or ask an administrator for access.", {email: dom('b', user.email)})),
] : [
// This page is not normally shown because a logged out user with no access will get
// redirected to log in. But it may be seen if a user logs out and returns to a cached
@ -58,7 +59,7 @@ export function createForbiddenPage(appModel: AppModel, message?: string) {
export function createSignedOutPage(appModel: AppModel) {
document.title = t("Signed out{{suffix}}", {suffix: getPageTitleSuffix(getGristConfig())});
return pagePanelsError(appModel, t("Signed out{{suffix}}", {suffix: ''}), [
return pagePanelsError(appModel, t("Signed out{{suffix}}", {suffix: ''}), [
cssErrorText(t("You are now signed out.")),
cssButtonWrap(bigPrimaryButtonLink(
t("Sign in again"), {href: getLoginUrl()}, testId('error-signin')
@ -73,7 +74,10 @@ export function createNotFoundPage(appModel: AppModel, message?: string) {
document.title = t("Page not found{{suffix}}", {suffix: getPageTitleSuffix(getGristConfig())});
return pagePanelsError(appModel, t("Page not found{{suffix}}", {suffix: ''}), [
cssErrorText(message || t("The requested page could not be found.{{separator}}Please check the URL and try again.", {separator: dom('br')})), // TODO: i18next
cssErrorText(message ||
t("The requested page could not be found.{{separator}}Please check the URL and try again.", {
separator: dom('br')
})),
cssButtonWrap(bigPrimaryButtonLink(t("Go to main page"), testId('error-primary-btn'),
urlState().setLinkUrl({}))),
cssButtonWrap(bigBasicButtonLink(t("Contact support"), {href: 'https://getgrist.com/contact'})),

@ -1,5 +1,5 @@
import {theme, vars} from 'app/client/ui2018/cssVars';
import {dom, IDomArgs, DomElementArg, IInputOptions, Observable, styled, subscribe} from 'grainjs';
import {dom, DomElementArg, IDomArgs, IInputOptions, Observable, styled, subscribe} from 'grainjs';
export const cssInput = styled('input', `
font-size: ${vars.mediumFontSize};

@ -1,4 +1,4 @@
import { ACResults, buildHighlightedDom, normalizeText, HighlightFunc } from 'app/client/lib/ACIndex';
import { ACResults, buildHighlightedDom, HighlightFunc, normalizeText } from 'app/client/lib/ACIndex';
import { Autocomplete } from 'app/client/lib/autocomplete';
import { ICellItem } from 'app/client/models/ColumnACIndexes';
import { reportError } from 'app/client/models/errors';

@ -1,5 +1,5 @@
import { createGroup } from 'app/client/components/commands';
import { ACItem, ACResults, normalizeText, HighlightFunc } from 'app/client/lib/ACIndex';
import { ACItem, ACResults, HighlightFunc, normalizeText } from 'app/client/lib/ACIndex';
import { IAutocompleteOptions } from 'app/client/lib/autocomplete';
import { IToken, TokenField, tokenFieldStyles } from 'app/client/lib/TokenField';
import { reportError } from 'app/client/models/errors';

@ -9,5 +9,5 @@ export default function getCurrentTime(): moment.Moment {
if (typeof window === 'undefined' || !window) { return getDefault(); }
const searchParams = new URLSearchParams(window.location.search);
return searchParams.has('currentTime') ? moment(searchParams.get('currentTime')!) : getDefault();
return searchParams.has('currentTime') ? moment(searchParams.get('currentTime')) : getDefault();
}

@ -18,7 +18,7 @@ import {makeId} from 'app/server/lib/idUtils';
import log from 'app/server/lib/log';
import {IPermitStore, Permit} from 'app/server/lib/Permit';
import {AccessTokenInfo} from 'app/server/lib/AccessTokens';
import {allowHost, isEnvironmentAllowedHost, getOriginUrl, optStringParam} from 'app/server/lib/requestUtils';
import {allowHost, getOriginUrl, isEnvironmentAllowedHost, optStringParam} from 'app/server/lib/requestUtils';
import * as cookie from 'cookie';
import {NextFunction, Request, RequestHandler, Response} from 'express';
import {IncomingMessage} from 'http';

@ -955,8 +955,8 @@ export class DocWorkerApi {
const options: DownloadOptions = {
...params,
filename: name + (params.tableId === name ? '' : '-' + params.tableId),
}
return options
};
return options;
}
private _getActiveDoc(req: RequestWithLogin): Promise<ActiveDoc> {

@ -1,7 +1,7 @@
import {ApiError} from 'app/common/ApiError';
import {createFormatter} from 'app/common/ValueFormatter';
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {ExportData, exportSection, exportTable, Filter, DownloadOptions} from 'app/server/lib/Export';
import {DownloadOptions, ExportData, exportSection, exportTable, Filter} from 'app/server/lib/Export';
import log from 'app/server/lib/log';
import * as bluebird from 'bluebird';
import contentDisposition from 'content-disposition';

@ -1,6 +1,6 @@
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
import {createExcelFormatter} from 'app/server/lib/ExcelFormatter';
import {ExportData, exportDoc, DownloadOptions, exportSection, exportTable, Filter} from 'app/server/lib/Export';
import {DownloadOptions, ExportData, exportDoc, exportSection, exportTable, Filter} from 'app/server/lib/Export';
import {Alignment, Border, Fill, Workbook} from 'exceljs';
import * as express from 'express';
import log from 'app/server/lib/log';

@ -376,7 +376,7 @@ export class DocTriggers {
private async _pushToRedisQueue(events: WebHookEvent[]) {
const strings = events.map(e => JSON.stringify(e));
await this._redisClient!.rpushAsync(this._redisQueueKey, ...strings);
await this._redisClient?.rpushAsync(this._redisQueueKey, ...strings);
}
private async _getRedisQueue(redisClient: RedisClient) {

@ -108,7 +108,9 @@ export function allowHost(req: Request, allowedHost: string|URL) {
// For requests to a native subdomains, only the base domain needs to match.
const allowedDomain = parseSubdomain(allowedUrl.hostname);
const actualDomain = parseSubdomain(actualUrl.hostname);
return (!_.isEmpty(actualDomain) ? actualDomain.base === allowedDomain.base : allowedUrl.hostname === actualUrl.hostname);
return (!_.isEmpty(actualDomain) ?
actualDomain.base === allowedDomain.base :
allowedUrl.hostname === actualUrl.hostname);
}
}

@ -21,6 +21,9 @@
"test:docker": "./test/test_under_docker.sh",
"test:python": "sandbox_venv3/bin/python sandbox/grist/runtests.py ${GREP_TESTS:+discover -p \"test*${GREP_TESTS}*.py\"}",
"cli": "NODE_PATH=_build:_build/stubs:_build/ext node _build/app/server/companion.js",
"lint": "eslint --cache --cache-strategy content .",
"lint:fix": "eslint --cache --cache-strategy=content --fix .",
"lint:ci": "eslint --max-warnings=0 .",
"generate:translation": "NODE_PATH=_build:_build/stubs:_build/ext node buildtools/generate_translation_keys.js"
},
"keywords": [
@ -34,6 +37,8 @@
},
"private": false,
"devDependencies": {
"@babel/core": "7.18.5",
"@babel/eslint-parser": "7.18.2",
"@types/accept-language-parser": "1.5.2",
"@types/backbone": "1.3.43",
"@types/chai": "4.1.7",
@ -71,12 +76,15 @@
"@types/uuid": "3.4.4",
"@types/which": "2.0.1",
"@types/ws": "^6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"app-module-path": "2.2.0",
"catw": "1.0.1",
"chai": "4.2.0",
"chai-as-promised": "7.1.1",
"chance": "1.0.16",
"esbuild-loader": "2.19.0",
"eslint": "8.18.0",
"http-proxy": "1.18.1",
"i18next-scanner": "4.1.0",
"jsdom": "16.5.0",

@ -0,0 +1,5 @@
module.exports = {
rules: {
'@typescript-eslint/no-shadow': 'off',
},
}

@ -2,14 +2,14 @@ import {makeT, t} from 'app/client/lib/localization';
import {assert} from 'chai';
import i18next, {i18n} from 'i18next';
import {Disposable, dom, DomContents, observable} from "grainjs";
import {popGlobals, pushGlobals, G} from 'grainjs/dist/cjs/lib/browserGlobals';
import {G, popGlobals, pushGlobals} from 'grainjs/dist/cjs/lib/browserGlobals';
import {JSDOM} from 'jsdom';
describe('localization', function() {
let instance: i18n;
before(() => {
before(async () => {
instance = i18next.createInstance();
instance.init({
await instance.init({
lng: 'en',
resources: {
en: {

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-shadow */
import {ActionSummary} from 'app/common/ActionSummary';
import {BulkColValues, UserAction} from 'app/common/DocActions';
import {arrayRepeat} from 'app/common/gutil';
@ -2103,7 +2104,10 @@ function testDocApi() {
});
it("GET /docs/{did}/download/xlsx returns 404 if tableId is invalid", async function() {
const resp = await axios.get(`${serverUrl}/api/docs/${docIds.TestDoc}/download/xlsx?tableId=MissingTableId`, chimpy);
const resp = await axios.get(
`${serverUrl}/api/docs/${docIds.TestDoc}/download/xlsx?tableId=MissingTableId`,
chimpy
);
assert.equal(resp.status, 404);
assert.deepEqual(resp.data, { error: 'Table MissingTableId not found.' });
});

@ -0,0 +1,15 @@
{
"extends": "./buildtools/tsconfig-base.json",
"include": [
"app/**/*.js",
"app/**/*.ts",
"stubs/**/*.ts",
"stubs/**/*.ts",
"test/**/*.js",
"test/**/*.ts",
"plugins/**/*.js",
"plugins/**/*.ts",
"buildtools/**/*.js",
"buildtools/**/*.ts",
]
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save