(core) updates from grist-core

This commit is contained in:
Paul Fitzpatrick 2023-01-04 10:38:22 -05:00
commit cd72a54bbb
41 changed files with 1079 additions and 101 deletions

11
.eslintignore Normal file
View File

@ -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

122
.eslintrc.js Normal file
View File

@ -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',
},
}]
}

View File

@ -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

View File

@ -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: []};

View File

@ -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;

View File

@ -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
),
]),

View File

@ -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) {

View File

@ -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, "),

View File

@ -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}`);
}

View File

@ -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) {

View File

@ -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> {
@ -29,7 +30,9 @@ export function buildMultiUserManagerModal(
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());
@ -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(

View File

@ -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!);
}
};
}

View File

@ -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 () => {
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')) : null,
}),
testId('modal-recovery-mode')
)
},
);
}

View File

@ -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),
),

View File

@ -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?")
);
}
}

View File

@ -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`);
}
}

View File

@ -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,
}];

View File

@ -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(

View File

@ -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"),
},
}, {

View File

@ -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");

View File

@ -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) {

View File

@ -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')))
);
}

View File

@ -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>;

View File

@ -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
@ -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'})),

View File

@ -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};

View File

@ -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';

View File

@ -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';

View File

@ -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();
}

View File

@ -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';

View File

@ -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> {

View File

@ -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';

View File

@ -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';

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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",

5
test/.eslintrc.js Normal file
View File

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

View File

@ -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: {

View File

@ -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.' });
});

15
tsconfig.eslint.json Normal file
View File

@ -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",
]
}

814
yarn.lock

File diff suppressed because it is too large Load Diff