(core) updates from grist-core

This commit is contained in:
Alex Hall 2023-07-28 17:40:31 +02:00
commit 9b87a6f06a
10 changed files with 52 additions and 50 deletions

View File

@ -244,7 +244,7 @@ export class SelectionSummary extends Disposable {
} else { } else {
for (const i of rowIndices) { for (const i of rowIndices) {
const value = values[i]; const value = values[i];
if (value !== null && value !== undefined && value !== '' && !isEmpty?.(value)) { if (value !== null && value !== undefined && value !== '' && value !== false && !isEmpty?.(value)) {
countNonEmpty++; countNonEmpty++;
} }
} }

View File

@ -77,13 +77,23 @@ export class UrlState<IUrlState extends object> extends Disposable {
if (samePage) { if (samePage) {
await this._stateImpl.delayPushUrl(prevState, newState); await this._stateImpl.delayPushUrl(prevState, newState);
if (options.replace) { try {
this._window.history.replaceState(null, '', newUrl); if (options.replace) {
} else { this._window.history.replaceState(null, '', newUrl);
this._window.history.pushState(null, '', newUrl); } else {
this._window.history.pushState(null, '', newUrl);
}
// pushState/replaceState above do not trigger 'popstate' event, so we call loadState() manually.
this.loadState();
} catch (e) {
// If we fail, we may be in a context where Grist doesn't have
// control over history, e.g. an iframe with srcdoc. Go ahead
// and apply the application state change (e.g. switching to a
// different Grist page). The back button won't work, but what
// it should do in an embedded context is full of nuance anyway.
log.debug(`pushUrl failure: ${e}`);
this.state.set(this._stateImpl.decodeUrl(new URL(newUrl)));
} }
// pushState/replaceState above do not trigger 'popstate' event, so we call loadState() manually.
this.loadState();
} else { } else {
this._window._urlStateLoadPage!(newUrl); this._window._urlStateLoadPage!(newUrl);
} }

View File

@ -119,13 +119,13 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
public readonly currentWorkspace = Computed.create(this, this.currentDoc, (use, doc) => doc && doc.workspace); public readonly currentWorkspace = Computed.create(this, this.currentDoc, (use, doc) => doc && doc.workspace);
public readonly currentOrg = Computed.create(this, this.currentWorkspace, (use, ws) => ws && ws.org); public readonly currentOrg = Computed.create(this, this.currentWorkspace, (use, ws) => ws && ws.org);
public readonly currentOrgName = Computed.create(this, this.currentOrg, public readonly currentOrgName = Computed.create(this, this.currentOrg,
(use, org) => getOrgNameOrGuest(org, this.appModel.currentUser)); (use, org) => getOrgNameOrGuest(org, this.appModel.currentUser));
public readonly currentDocTitle = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.name : ''); public readonly currentDocTitle = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.name : '');
public readonly isReadonly = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isReadonly : false); public readonly isReadonly = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isReadonly : false);
public readonly isPrefork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isPreFork : false); public readonly isPrefork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isPreFork : false);
public readonly isFork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isFork : false); public readonly isFork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isFork : false);
public readonly isRecoveryMode = Computed.create(this, this.currentDoc, public readonly isRecoveryMode = Computed.create(this, this.currentDoc,
(use, doc) => doc ? doc.isRecoveryMode : false); (use, doc) => doc ? doc.isRecoveryMode : false);
public readonly userOverride = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.userOverride : null); public readonly userOverride = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.userOverride : null);
public readonly isBareFork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isBareFork : false); public readonly isBareFork = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isBareFork : false);
public readonly isSnapshot = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isSnapshot : false); public readonly isSnapshot = Computed.create(this, this.currentDoc, (use, doc) => doc ? doc.isSnapshot : false);
@ -173,7 +173,7 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
FlowRunner.create(this._openerHolder, FlowRunner.create(this._openerHolder,
(flow: AsyncFlow) => this._openDoc(flow, urlId, urlOpenMode, state.params?.compare, linkParameters) (flow: AsyncFlow) => this._openDoc(flow, urlId, urlOpenMode, state.params?.compare, linkParameters)
) )
.resultPromise.catch(err => this._onOpenError(err)); .resultPromise.catch(err => this._onOpenError(err));
} }
} }
})); }));
@ -270,7 +270,7 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
explanation: ( explanation: (
isDocOwner isDocOwner
? t("You can try reloading the document, or using recovery mode. \ ? 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. \ Recovery mode opens the document to be fully accessible to owners, and inaccessible to others. \
It also disables formulas. [{{error}}]", {error: err.message}) It also disables formulas. [{{error}}]", {error: err.message})
: isDenied : isDenied
? t('Sorry, access to this document has been denied. [{{error}}]', {error: err.message}) ? t('Sorry, access to this document has been denied. [{{error}}]', {error: err.message})
@ -305,7 +305,7 @@ It also disables formulas. [{{error}}]", {error: err.message})
comparisonUrlId: string | undefined, comparisonUrlId: string | undefined,
linkParameters: Record<string, string> | undefined): Promise<void> { linkParameters: Record<string, string> | undefined): Promise<void> {
console.log(`DocPageModel _openDoc starting for ${urlId} (mode ${urlOpenMode})` + console.log(`DocPageModel _openDoc starting for ${urlId} (mode ${urlOpenMode})` +
(comparisonUrlId ? ` (compare ${comparisonUrlId})` : '')); (comparisonUrlId ? ` (compare ${comparisonUrlId})` : ''));
const gristDocModulePromise = loadGristDoc(); const gristDocModulePromise = loadGristDoc();
const docResponse = await retryOnNetworkError(flow, getDoc.bind(null, this._api, urlId)); const docResponse = await retryOnNetworkError(flow, getDoc.bind(null, this._api, urlId));
@ -379,7 +379,7 @@ It also disables formulas. [{{error}}]", {error: err.message})
await this._api.getDocAPI(urlId).compareDoc(comparisonUrlId, { detail: true }) : undefined; await this._api.getDocAPI(urlId).compareDoc(comparisonUrlId, { detail: true }) : undefined;
const gristDoc = gdModule.GristDoc.create(flow, this._appObj, this.appModel, docComm, this, openDocResponse, const gristDoc = gdModule.GristDoc.create(flow, this._appObj, this.appModel, docComm, this, openDocResponse,
this.appModel.topAppModel.plugins, {comparison}); this.appModel.topAppModel.plugins, {comparison});
// Move ownership of docComm to GristDoc. // Move ownership of docComm to GristDoc.
gristDoc.autoDispose(flow.release(docComm)); gristDoc.autoDispose(flow.release(docComm));
@ -403,13 +403,13 @@ function addMenu(importSources: ImportSource[], gristDoc: GristDoc, isReadonly:
return [ return [
menuItem( menuItem(
(elem) => openPageWidgetPicker(elem, gristDoc, (val) => gristDoc.addNewPage(val).catch(reportError), (elem) => openPageWidgetPicker(elem, gristDoc, (val) => gristDoc.addNewPage(val).catch(reportError),
{isNewPage: true, buttonLabel: 'Add Page'}), {isNewPage: true, buttonLabel: 'Add Page'}),
menuIcon("Page"), t("Add Page"), testId('dp-add-new-page'), menuIcon("Page"), t("Add Page"), testId('dp-add-new-page'),
dom.cls('disabled', isReadonly) dom.cls('disabled', isReadonly)
), ),
menuItem( menuItem(
(elem) => openPageWidgetPicker(elem, gristDoc, (val) => gristDoc.addWidgetToPage(val).catch(reportError), (elem) => openPageWidgetPicker(elem, gristDoc, (val) => gristDoc.addWidgetToPage(val).catch(reportError),
{isNewPage: false, selectBy}), {isNewPage: false, selectBy}),
menuIcon("Widget"), t("Add Widget to Page"), testId('dp-add-widget-to-page'), menuIcon("Widget"), t("Add Widget to Page"), testId('dp-add-widget-to-page'),
// disable for readonly doc and all special views // disable for readonly doc and all special views
dom.cls('disabled', (use) => typeof use(gristDoc.activeViewId) !== 'number' || isReadonly), dom.cls('disabled', (use) => typeof use(gristDoc.activeViewId) !== 'number' || isReadonly),

View File

@ -657,8 +657,9 @@ def TASTEME(food):
@unimplemented @unimplemented
def TEXT(number, format_type): # pylint: disable=unused-argument def TEXT(number, format_type): # pylint: disable=unused-argument
""" """
Converts a number into text according to a specified format. It is not yet implemented in Converts a number into text according to a specified format. It is not yet implemented in
Grist. Grist. You can use the similar Python functions str() to convert numbers into strings, and
optionally format() to specify the number format.
""" """
raise NotImplementedError() raise NotImplementedError()

View File

@ -397,7 +397,8 @@
"GristDoc": { "GristDoc": {
"Added new linked section to view {{viewName}}": "Added new linked section to view {{viewName}}", "Added new linked section to view {{viewName}}": "Added new linked section to view {{viewName}}",
"Import from file": "Import from file", "Import from file": "Import from file",
"Saved linked section {{title}} in view {{name}}": "Saved linked section {{title}} in view {{name}}" "Saved linked section {{title}} in view {{name}}": "Saved linked section {{title}} in view {{name}}",
"go to webhook settings": "go to webhook settings"
}, },
"HomeIntro": { "HomeIntro": {
"Any documents created in this site will appear here.": "Any documents created in this site will appear here.", "Any documents created in this site will appear here.": "Any documents created in this site will appear here.",

Binary file not shown.

View File

@ -391,6 +391,7 @@ describe('Dates.ntest', function() {
await gu.clickCellRC(0, 1); await gu.clickCellRC(0, 1);
await gu.sendKeys([$.ALT, '=']); await gu.sendKeys([$.ALT, '=']);
await gu.waitForServer(); await gu.waitForServer();
await gu.waitAppFocus(false);
await gu.sendKeys("Diff", $.ENTER); await gu.sendKeys("Diff", $.ENTER);
await gu.waitForServer(); await gu.waitForServer();
await gu.sendKeys('='); await gu.sendKeys('=');

View File

@ -4,7 +4,7 @@ import * as gu from 'test/nbrowser/gristUtils';
import {server, setupTestSuite} from 'test/nbrowser/testUtils'; import {server, setupTestSuite} from 'test/nbrowser/testUtils';
describe('SelectByRefList', function() { describe('SelectByRefList', function() {
this.timeout(60000); this.timeout(80000);
setupTestSuite(); setupTestSuite();
addToRepl('gu2', gu); addToRepl('gu2', gu);
gu.bigScreen(); gu.bigScreen();

View File

@ -130,11 +130,6 @@ describe('SelectionSummary', function () {
count: 2, count: 2,
sum: null, sum: null,
}); });
await selectAndAssert({col: 2, row: 0}, {col: 3, row: 5}, {
dimensions: '62',
count: 11,
sum: null,
});
// Scroll horizontally to the end of the table. // Scroll horizontally to the end of the table.
await gu.sendKeys(Key.END); await gu.sendKeys(Key.END);
@ -151,6 +146,15 @@ describe('SelectionSummary', function () {
}); });
}); });
it('does not count false values', async function () {
// False values in boolean columns should not be included in count
await selectAndAssert({col: 2, row: 0}, {col: 3, row: 5}, {
dimensions: '62',
count: 9,
sum: null,
});
});
it('uses the show column of reference columns for computations', async function () { it('uses the show column of reference columns for computations', async function () {
// Column 6 is a Reference column pointing to column 0. // Column 6 is a Reference column pointing to column 0.
await gu.sendKeys(Key.HOME); await gu.sendKeys(Key.HOME);

View File

@ -16,7 +16,6 @@ describe('WebhookOverflow', function () {
before(async function () { before(async function () {
oldEnv = new EnvironmentSnapshot(); oldEnv = new EnvironmentSnapshot();
//host = new URL(server.getHost()).host;
process.env.ALLOWED_WEBHOOK_DOMAINS = '*'; process.env.ALLOWED_WEBHOOK_DOMAINS = '*';
process.env.GRIST_MAX_QUEUE_SIZE = '2'; process.env.GRIST_MAX_QUEUE_SIZE = '2';
await server.restart(); await server.restart();
@ -50,45 +49,31 @@ describe('WebhookOverflow', function () {
await driver.sendKeys(...keys); await driver.sendKeys(...keys);
} }
it('should show a message when overflown', async function () { it('should show a message when overflowed', async function () {
await gu.openPage('Table2'); await gu.openPage('Table2');
await gu.getCell('A', 1).click(); await gu.getCell('A', 1).click();
await gu.enterCell('123'); await gu.enterCell('123');
await gu.getCell('B', 1).click(); await gu.getCell('B', 1).click();
await enterCellWithoutWaitingOnServer('124'); await enterCellWithoutWaitingOnServer('124');
const toast = await driver.wait(() => gu.getToasts(), 10000); await gu.waitToPass(async () => {
assert.include(toast, 'New changes are temporarily suspended. Webhooks queue overflowed.' + const toast = await gu.getToasts();
' Please check webhooks settings, remove invalid webhooks, and clean the queue.\ngo to webhook settings'); assert.include(toast, 'New changes are temporarily suspended. Webhooks queue overflowed.' +
' Please check webhooks settings, remove invalid webhooks, and clean the queue.\ngo to webhook settings');
}, 4000);
}); });
it('message should disappear after clearing queue', async function () { it('message should disappear after clearing queue', async function () {
await openWebhookPageWithoutWaitForServer(); await openWebhookPageWithoutWaitForServer();
await driver.findContent('button', /Clear Queue/).click(); await driver.findContent('button', /Clear Queue/).click();
await gu.waitForServer(); await gu.waitForServer();
await waitForOverflownMessageToDisappear(); await gu.waitToPass(async () => {
const toast = await driver.wait(() => gu.getToasts()); const toast = await gu.getToasts();
assert.notInclude(toast, 'New changes are temporarily suspended. Webhooks queue overflowed.' + assert.notInclude(toast, 'New changes are temporarily suspended. Webhooks queue overflowed.' +
' Please check webhooks settings, remove invalid webhooks, and clean the queue.\ngo to webhook settings'); ' Please check webhooks settings, remove invalid webhooks, and clean the queue.\ngo to webhook settings');
}, 12500);
}); });
}); });
async function waitForOverflownMessageToDisappear(maxWait = 12500) {
await driver.wait(async () => {
try {
for (;;) {
const toasts = await gu.getToasts();
const filteredToasts = toasts.find(t => t=='New changes are temporarily suspended. Webhooks queue overflowed.' +
' Please check webhooks settings, remove invalid webhooks, and clean the queue.\ngo to webhook settings');
if (!filteredToasts) {
return true;
}
}
} catch (e) {
return false;
}
}, maxWait, 'Overflown message did not disappear');
}
async function openWebhookPageWithoutWaitForServer() { async function openWebhookPageWithoutWaitForServer() {
await openDocumentSettings(); await openDocumentSettings();
const button = await driver.findContentWait('a', /Manage Webhooks/, 3000); const button = await driver.findContentWait('a', /Manage Webhooks/, 3000);