mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) updates from grist-core
This commit is contained in:
commit
9b87a6f06a
@ -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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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.",
|
||||||
|
BIN
test/fixtures/docs/SelectBySummary.grist
vendored
BIN
test/fixtures/docs/SelectBySummary.grist
vendored
Binary file not shown.
@ -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('=');
|
||||||
|
@ -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();
|
||||||
|
@ -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: '6⨯2',
|
|
||||||
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: '6⨯2',
|
||||||
|
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);
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user