(core) deleting queue from single webhook

Summary: Using standard tost notification, message about webhook queue being overflown was added. message is permanent as long as queue is full. Message contains linkt to the webhook setings

Test Plan: two nbrowser test was added - one to check if message is show when queue is full, and second to check if message is dismiss when queue was cleaned.

Reviewers: georgegevoian

Reviewed By: georgegevoian

Subscribers: jarek

Differential Revision: https://phab.getgrist.com/D3929
This commit is contained in:
Jakub Serafin
2023-07-18 09:24:10 +02:00
parent 450472f74c
commit d894b60fd4
14 changed files with 475 additions and 151 deletions

View File

@@ -18,7 +18,6 @@ import {EditorMonitor} from "app/client/components/EditorMonitor";
import * as GridView from 'app/client/components/GridView';
import {Importer} from 'app/client/components/Importer';
import {RawDataPage, RawDataPopup} from 'app/client/components/RawDataPage';
import {DocSettingsPage} from 'app/client/ui/DocumentSettings';
import {ActionGroupWithCursorPos, UndoStack} from 'app/client/components/UndoStack';
import {ViewLayout} from 'app/client/components/ViewLayout';
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
@@ -43,6 +42,7 @@ import {App} from 'app/client/ui/App';
import {DocHistory} from 'app/client/ui/DocHistory';
import {startDocTour} from "app/client/ui/DocTour";
import {DocTutorial} from 'app/client/ui/DocTutorial';
import {DocSettingsPage} from 'app/client/ui/DocumentSettings';
import {isTourActive} from "app/client/ui/OnBoardingPopups";
import {IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
import {linkFromId, selectBy} from 'app/client/ui/selectBy';
@@ -54,8 +54,8 @@ import {isNarrowScreen, mediaSmall, mediaXSmall, testId, theme} from 'app/client
import {IconName} from 'app/client/ui2018/IconList';
import {icon} from 'app/client/ui2018/icons';
import {invokePrompt} from 'app/client/ui2018/modals';
import {FieldEditor} from "app/client/widgets/FieldEditor";
import {DiscussionPanel} from 'app/client/widgets/DiscussionEditor';
import {FieldEditor} from "app/client/widgets/FieldEditor";
import {MinimalActionGroup} from 'app/common/ActionGroup';
import {ClientQuery} from "app/common/ActiveDocAPI";
import {CommDocChatter, CommDocUsage, CommDocUserAction} from 'app/common/CommTypes';
@@ -120,8 +120,9 @@ const RightPanelTool = StringUnion("none", "docHistory", "validations", "discuss
export interface IExtraTool {
icon: IconName;
label: DomContents;
content: TabContent[]|IDomComponent;
content: TabContent[] | IDomComponent;
}
interface RawSectionOptions {
viewSection: ViewSectionRec;
hash: HashLink;
@@ -137,10 +138,10 @@ export class GristDoc extends DisposableWithEvents {
public docInfo: DocInfoRec;
public docPluginManager: DocPluginManager;
public querySetManager: QuerySetManager;
public rightPanelTool: Observable<IExtraTool|null>;
public rightPanelTool: Observable<IExtraTool | null>;
public isReadonly = this.docPageModel.isReadonly;
public isReadonlyKo = toKo(ko, this.isReadonly);
public comparison: DocStateComparison|null;
public comparison: DocStateComparison | null;
// component for keeping track of latest cursor position
public cursorMonitor: CursorMonitor;
// component for keeping track of a cell that is being edited
@@ -174,11 +175,11 @@ export class GristDoc extends DisposableWithEvents {
// section (or raw data section) that is not part of this view. Maximized section is a section
// in the view, so there is no need to render it twice, layout just hides all other sections to make
// the space.
public maximizedSectionId: Observable<number|null> = Observable.create(this, null);
public maximizedSectionId: Observable<number | null> = Observable.create(this, null);
// This is id of the section that is currently shown in the popup. Probably this is an external
// section, like raw data view, or a section from another view..
public externalSectionId: Computed<number|null>;
public viewLayout: ViewLayout|null = null;
public externalSectionId: Computed<number | null>;
public viewLayout: ViewLayout | null = null;
// Holder for the popped up formula editor.
public readonly formulaPopup = Holder.create(this);
@@ -191,15 +192,15 @@ export class GristDoc extends DisposableWithEvents {
private _actionLog: ActionLog;
private _undoStack: UndoStack;
private _lastOwnActionGroup: ActionGroupWithCursorPos|null = null;
private _lastOwnActionGroup: ActionGroupWithCursorPos | null = null;
private _rightPanelTabs = new Map<string, TabContent[]>();
private _docHistory: DocHistory;
private _discussionPanel: DiscussionPanel;
private _rightPanelTool = createSessionObs(this, "rightPanelTool", "none", RightPanelTool.guard);
private _showGristTour = getUserOrgPrefObs(this.userOrgPrefs, 'showGristTour');
private _seenDocTours = getUserOrgPrefObs(this.userOrgPrefs, 'seenDocTours');
private _rawSectionOptions: Observable<RawSectionOptions|null> = Observable.create(this, null);
private _activeContent: Computed<IDocPage|RawSectionOptions>;
private _rawSectionOptions: Observable<RawSectionOptions | null> = Observable.create(this, null);
private _activeContent: Computed<IDocPage | RawSectionOptions>;
private _docTutorialHolder = Holder.create<DocTutorial>(this);
private _isRickRowing: Observable<boolean> = Observable.create(this, false);
private _showBackgroundVideoPlayer: Observable<boolean> = Observable.create(this, false);
@@ -285,9 +286,13 @@ export class GristDoc extends DisposableWithEvents {
}
}));
// Subscribe to URL state, and navigate to anchor or open a popup if necessary.
this.autoDispose(subscribe(urlState().state, async (use, state) => {
if (!state.hash) { return; }
if (!state.hash) {
return;
}
try {
if (state.hash.popup) {
@@ -321,7 +326,9 @@ export class GristDoc extends DisposableWithEvents {
this._waitForView()
.then(() => {
const cursor = document.querySelector('.selected_cursor.active_cursor');
if (!cursor) { return; }
if (!cursor) {
return;
}
this.behavioralPromptsManager.showTip(cursor, 'rickRow', {
forceShow: true,
@@ -427,8 +434,8 @@ export class GristDoc extends DisposableWithEvents {
// Set the available import sources in the DocPageModel.
this.docPageModel.importSources = importMenuItems;
this._actionLog = this.autoDispose(ActionLog.create({ gristDoc: this }));
this._undoStack = this.autoDispose(UndoStack.create(openDocResponse.log, { gristDoc: this }));
this._actionLog = this.autoDispose(ActionLog.create({gristDoc: this}));
this._undoStack = this.autoDispose(UndoStack.create(openDocResponse.log, {gristDoc: this}));
this._docHistory = DocHistory.create(this, this.docPageModel, this._actionLog);
this._discussionPanel = DiscussionPanel.create(this, this);
@@ -439,9 +446,15 @@ export class GristDoc extends DisposableWithEvents {
/* Command binding */
this.autoDispose(commands.createGroup({
undo() { this._undoStack.sendUndoAction().catch(reportError); },
redo() { this._undoStack.sendRedoAction().catch(reportError); },
reloadPlugins() { void this.docComm.reloadPlugins().then(() => G.window.location.reload(false)); },
undo() {
this._undoStack.sendUndoAction().catch(reportError);
},
redo() {
this._undoStack.sendRedoAction().catch(reportError);
},
reloadPlugins() {
void this.docComm.reloadPlugins().then(() => G.window.location.reload(false));
},
// Command to be manually triggered on cell selection. Moves the cursor to the selected cell.
// This is overridden by the formula editor to insert "$col" variables when clicking cells.
@@ -454,6 +467,8 @@ export class GristDoc extends DisposableWithEvents {
this.listenTo(app.comm, 'docChatter', this.onDocChatter);
this._handleTriggerQueueOverflowMessage();
this.autoDispose(DocConfigTab.create({gristDoc: this}));
this.rightPanelTool = Computed.create(this, (use) => this._getToolContent(use(this._rightPanelTool)));
@@ -478,7 +493,9 @@ export class GristDoc extends DisposableWithEvents {
// switch for a moment to knockout to fix this.
const viewInstance = fromKo(this.autoDispose(ko.pureComputed(() => {
const viewId = toKo(ko, this.activeViewId)();
if (!isViewDocPage(viewId)) { return null; }
if (!isViewDocPage(viewId)) {
return null;
}
const section = this.viewModel.activeSection();
if (section?.isDisposed()) { return null; }
const view = section.viewInstance();
@@ -490,7 +507,9 @@ export class GristDoc extends DisposableWithEvents {
if (view) {
await view.getLoadingDonePromise();
}
if (view?.isDisposed()) { return; }
if (view?.isDisposed()) {
return;
}
// finally set the current view as fully loaded
this.currentView.set(view);
}));
@@ -499,12 +518,18 @@ export class GristDoc extends DisposableWithEvents {
this.cursorPosition = Computed.create<ViewCursorPos | undefined>(this, use => {
// get the BaseView
const view = use(this.currentView);
if (!view) { return undefined; }
if (!view) {
return undefined;
}
const viewId = use(this.activeViewId);
if (!isViewDocPage(viewId)) { return undefined; }
if (!isViewDocPage(viewId)) {
return undefined;
}
// read latest position
const currentPosition = use(view.cursor.currentPosition);
if (currentPosition) { return { ...currentPosition, viewId }; }
if (currentPosition) {
return {...currentPosition, viewId};
}
return undefined;
});
@@ -520,7 +545,9 @@ export class GristDoc extends DisposableWithEvents {
// When active section is changed to a chart or custom widget, change the tab in the creator
// panel to the table.
this.autoDispose(this.viewModel.activeSection.subscribe((section) => {
if (section.isDisposed() || section._isDeleted.peek()) { return; }
if (section.isDisposed() || section._isDeleted.peek()) {
return;
}
if (['chart', 'custom'].includes(section.parentKey.peek())) {
commands.allCommands.viewTabFocus.run();
}
@@ -609,12 +636,12 @@ export class GristDoc extends DisposableWithEvents {
* fields { sectionId, rowId, fieldIndex }. Fields may be missing if no section is active.
*/
public getCursorPos(): CursorPos {
const pos = { sectionId: this.viewModel.activeSectionId() };
const pos = {sectionId: this.viewModel.activeSectionId()};
const viewInstance = this.viewModel.activeSection.peek().viewInstance.peek();
return Object.assign(pos, viewInstance ? viewInstance.cursor.getCursorPos() : {});
}
public async onSetCursorPos(rowModel: BaseRowModel|undefined, fieldModel?: ViewFieldRec) {
public async onSetCursorPos(rowModel: BaseRowModel | undefined, fieldModel?: ViewFieldRec) {
return this.setCursorPos({
rowIndex: rowModel?._index() || 0,
fieldIndex: fieldModel?._index() || 0,
@@ -669,7 +696,7 @@ export class GristDoc extends DisposableWithEvents {
}
try {
await this.setCursorPos(cursorPos);
} catch(e) {
} catch (e) {
reportError(e);
}
}
@@ -732,7 +759,9 @@ export class GristDoc extends DisposableWithEvents {
* observables.
*/
public onDocUsageMessage(message: CommDocUsage) {
if (!this.docComm.isActionFromThisDoc(message)) { return; }
if (!this.docComm.isActionFromThisDoc(message)) {
return;
}
bundleChanges(() => {
this.docPageModel.updateCurrentDocUsage(message.data.docUsage);
@@ -741,8 +770,15 @@ export class GristDoc extends DisposableWithEvents {
}
public onDocChatter(message: CommDocChatter) {
if (!this.docComm.isActionFromThisDoc(message)) { return; }
if (message.data.webhooks) {
if (!this.docComm.isActionFromThisDoc(message) ||
!message.data.webhooks) {
return;
}
if (message.data.webhooks.type == 'webhookOverflowError') {
this.trigger('webhookOverflowError',
t('New changes are temporarily suspended. Webhooks queue overflowed.' +
' Please check webhooks settings, remove invalid webhooks, and clean the queue.'),);
} else {
this.trigger('webhooks', message.data.webhooks);
}
}
@@ -755,7 +791,9 @@ export class GristDoc extends DisposableWithEvents {
// in effect.
public getTableModelMaybeWithDiff(tableId: string): DataTableModel {
const tableModel = this.getTableModel(tableId);
if (!this.comparison?.details) { return tableModel; }
if (!this.comparison?.details) {
return tableModel;
}
// TODO: cache wrapped models and share between views.
return new DataTableModelWithDiff(tableModel, this.comparison.details);
}
@@ -765,7 +803,9 @@ export class GristDoc extends DisposableWithEvents {
*/
public async addEmptyTable(): Promise<void> {
const name = await this._promptForName();
if (name === undefined) { return; }
if (name === undefined) {
return;
}
const tableInfo = await this.docData.sendAction(['AddEmptyTable', name || null]);
await this.openDocPage(this.docModel.tables.getRowModel(tableInfo.id).primaryViewId());
}
@@ -776,7 +816,7 @@ export class GristDoc extends DisposableWithEvents {
public async addWidgetToPage(val: IPageWidget) {
const docData = this.docModel.docData;
const viewName = this.viewModel.name.peek();
let tableId: string|null|undefined;
let tableId: string | null | undefined;
if (val.table === 'New Table') {
tableId = await this._promptForName();
if (tableId === undefined) {
@@ -797,7 +837,7 @@ export class GristDoc extends DisposableWithEvents {
/**
* The actual implementation of addWidgetToPage
*/
public async addWidgetToPageImpl(val: IPageWidget, tableId: string|null = null) {
public async addWidgetToPageImpl(val: IPageWidget, tableId: string | null = null) {
const viewRef = this.activeViewId.get();
const tableRef = val.table === 'New Table' ? 0 : val.table;
const result = await this.docData.sendAction(
@@ -816,7 +856,9 @@ export class GristDoc extends DisposableWithEvents {
public async addNewPage(val: IPageWidget) {
if (val.table === 'New Table') {
const name = await this._promptForName();
if (name === undefined) { return; }
if (name === undefined) {
return;
}
const result = await this.docData.sendAction(['AddEmptyTable', name]);
await this.openDocPage(result.views[0].id);
} else {
@@ -842,8 +884,10 @@ export class GristDoc extends DisposableWithEvents {
* primary view.
*/
public async uploadNewTable(): Promise<void> {
const uploadResult = await selectFiles({docWorkerUrl: this.docComm.docWorkerUrl,
multiple: true});
const uploadResult = await selectFiles({
docWorkerUrl: this.docComm.docWorkerUrl,
multiple: true
});
if (uploadResult) {
const dataSource = {uploadId: uploadResult.uploadId, transforms: []};
const importResult = await this.docComm.finishImportFiles(dataSource, [], {});
@@ -865,7 +909,7 @@ export class GristDoc extends DisposableWithEvents {
}
return await this.viewLayout!.freezeUntil(docData.bundleActions(
t("Saved linked section {{title}} in view {{name}}", {title:section.title(), name: viewModel.name()}),
t("Saved linked section {{title}} in view {{name}}", {title: section.title(), name: viewModel.name()}),
async () => {
// if table changes or a table is made a summary table, let's replace the view section by a
@@ -932,7 +976,9 @@ export class GristDoc extends DisposableWithEvents {
// adds new view fields; ignore colIds that do not exist in new table.
await Promise.all(colIds.map((colId, i) => {
if (!mapColIdToColumn.has(colId)) { return; }
if (!mapColIdToColumn.has(colId)) {
return;
}
const colInfo = {
parentId: section.id(),
colRef: mapColIdToColumn.get(colId).id(),
@@ -983,7 +1029,7 @@ export class GristDoc extends DisposableWithEvents {
}
// Turn the given columns into empty columns, losing any data stored in them.
public async clearColumns(colRefs: number[], {keepType}: {keepType?: boolean} = {}): Promise<void> {
public async clearColumns(colRefs: number[], {keepType}: { keepType?: boolean } = {}): Promise<void> {
await this.docModel.columns.sendTableAction(
['BulkUpdateRecord', colRefs, {
isFormula: colRefs.map(f => true),
@@ -1003,7 +1049,7 @@ export class GristDoc extends DisposableWithEvents {
}
// Convert the given columns to data, saving the calculated values and unsetting the formulas.
public async convertIsFormula(colRefs: number[], opts: {toFormula: boolean, noRecalc?: boolean}): Promise<void> {
public async convertIsFormula(colRefs: number[], opts: { toFormula: boolean, noRecalc?: boolean }): Promise<void> {
return this.docModel.columns.sendTableAction(
['BulkUpdateRecord', colRefs, {
isFormula: colRefs.map(f => opts.toFormula),
@@ -1076,8 +1122,12 @@ export class GristDoc extends DisposableWithEvents {
setAsActiveSection: boolean,
silent: boolean = false): Promise<boolean> {
try {
if (!cursorPos.sectionId) { throw new Error('sectionId required'); }
if (!cursorPos.rowId) { throw new Error('rowId required'); }
if (!cursorPos.sectionId) {
throw new Error('sectionId required');
}
if (!cursorPos.rowId) {
throw new Error('rowId required');
}
const section = this.docModel.viewSections.getRowModel(cursorPos.sectionId);
if (!section.id.peek()) {
throw new Error(`Section ${cursorPos.sectionId} does not exist`);
@@ -1125,7 +1175,9 @@ export class GristDoc extends DisposableWithEvents {
}
srcRowId = srcTable.getRowIds().find(getFilterFunc(this.docData, query));
}
if (!srcRowId || typeof srcRowId !== 'number') { throw new Error('cannot trace rowId'); }
if (!srcRowId || typeof srcRowId !== 'number') {
throw new Error('cannot trace rowId');
}
await this.recursiveMoveToCursorPos({
rowId: srcRowId,
sectionId: srcSection.id.peek(),
@@ -1133,14 +1185,20 @@ export class GristDoc extends DisposableWithEvents {
}
const view: ViewRec = section.view.peek();
const docPage: ViewDocPage = section.isRaw.peek() ? "data" : view.getRowId();
if (docPage != this.activeViewId.get()) { await this.openDocPage(docPage); }
if (setAsActiveSection) { view.activeSectionId(cursorPos.sectionId); }
if (docPage != this.activeViewId.get()) {
await this.openDocPage(docPage);
}
if (setAsActiveSection) {
view.activeSectionId(cursorPos.sectionId);
}
const fieldIndex = cursorPos.fieldIndex;
const viewInstance = await waitObs(section.viewInstance);
if (!viewInstance) { throw new Error('view not found'); }
if (!viewInstance) {
throw new Error('view not found');
}
// Give any synchronous initial cursor setting a chance to happen.
await delay(0);
viewInstance.setCursorPos({ ...cursorPos, fieldIndex });
viewInstance.setCursorPos({...cursorPos, fieldIndex});
// TODO: column selection not working on card/detail view, or getting overridden -
// look into it (not a high priority for now since feature not easily discoverable
// in this view).
@@ -1162,7 +1220,7 @@ export class GristDoc extends DisposableWithEvents {
* Opens up an editor at cursor position
* @param input Optional. Cell's initial value
*/
public async activateEditorAtCursor(options?: { init?: string, state?: any}) {
public async activateEditorAtCursor(options?: { init?: string, state?: any }) {
const view = await this._waitForView();
view?.activateEditorAtCursor(options);
}
@@ -1183,7 +1241,9 @@ export class GristDoc extends DisposableWithEvents {
*/
public async openPopup(hash: HashLink) {
// We can only open a popup for a section.
if (!hash.sectionId) { return; }
if (!hash.sectionId) {
return;
}
// We might open popup either for a section in this view or some other section (like Raw Data Page).
if (this.viewModel.viewSections.peek().peek().some(s => s.id.peek() === hash.sectionId)) {
if (this.viewLayout) {
@@ -1196,7 +1256,7 @@ export class GristDoc extends DisposableWithEvents {
const fieldIndex = activeSection.viewFields.peek().all().findIndex(f => f.colRef.peek() === hash.colRef);
if (fieldIndex >= 0) {
const view = await this._waitForView(activeSection);
view?.setCursorPos({ sectionId: hash.sectionId, rowId: hash.rowId, fieldIndex });
view?.setCursorPos({sectionId: hash.sectionId, rowId: hash.rowId, fieldIndex});
}
}
this.viewLayout?.maximized.set(hash.sectionId);
@@ -1219,17 +1279,23 @@ export class GristDoc extends DisposableWithEvents {
viewSection: popupSection,
close: () => {
// In case we are already close, do nothing.
if (!this._rawSectionOptions.get()) { return; }
if (!this._rawSectionOptions.get()) {
return;
}
if (popupSection !== prevSection) {
// We need to blur raw view section. Otherwise it will automatically be opened
// on raw data view. Note: raw data section doesn't have its own view, it uses
// empty row model as a parent (which feels like a hack).
if (!popupSection.isDisposed()) { popupSection.hasFocus(false); }
if (!popupSection.isDisposed()) {
popupSection.hasFocus(false);
}
// We need to restore active viewSection for a view that we borrowed.
// When this popup was opened we tricked active view by setting its activeViewSection
// to our viewSection (which might be a completely diffrent section or a raw data section) not
// connected to this view.
if (!prevSection.isDisposed()) { prevSection.hasFocus(true); }
if (!prevSection.isDisposed()) {
prevSection.hasFocus(true);
}
}
// Clearing popup data will close this popup.
this._rawSectionOptions.set(null);
@@ -1240,7 +1306,7 @@ export class GristDoc extends DisposableWithEvents {
const fieldIndex = popupSection.viewFields.peek().all().findIndex(f => f.colRef.peek() === hash.colRef);
if (fieldIndex >= 0) {
const view = await this._waitForView(popupSection);
view?.setCursorPos({ sectionId: hash.sectionId, rowId: hash.rowId, fieldIndex });
view?.setCursorPos({sectionId: hash.sectionId, rowId: hash.rowId, fieldIndex});
}
}
}
@@ -1250,7 +1316,9 @@ export class GristDoc extends DisposableWithEvents {
*/
public async playRickRollVideo() {
const backgroundVideoPlayer = this._backgroundVideoPlayerHolder.get();
if (!backgroundVideoPlayer) { return; }
if (!backgroundVideoPlayer) {
return;
}
await backgroundVideoPlayer.isLoaded();
backgroundVideoPlayer.play();
@@ -1272,7 +1340,9 @@ export class GristDoc extends DisposableWithEvents {
await setVolume(0, 100, 5);
await delay(190 * 1000);
if (!this._isRickRowing.get()) { return; }
if (!this._isRickRowing.get()) {
return;
}
await setVolume(100, 0, 5);
@@ -1289,6 +1359,7 @@ export class GristDoc extends DisposableWithEvents {
if (!sectionToCheck.getRowId()) {
return null;
}
async function singleWait(s: ViewSectionRec): Promise<BaseView> {
const view = await waitObs(
sectionToCheck.viewInstance,
@@ -1296,6 +1367,7 @@ export class GristDoc extends DisposableWithEvents {
);
return view!;
}
let view = await singleWait(sectionToCheck);
if (view.isDisposed()) {
// If the view is disposed (it can happen, as wait is not reliable enough, because it uses
@@ -1324,7 +1396,7 @@ export class GristDoc extends DisposableWithEvents {
return content ? {icon: 'Validation', label: 'Validation Rules', content} : null;
}
case 'discussion': {
return {icon: 'Chat', label: this._discussionPanel.buildMenu(), content: this._discussionPanel};
return {icon: 'Chat', label: this._discussionPanel.buildMenu(), content: this._discussionPanel};
}
case 'none':
default: {
@@ -1350,7 +1422,9 @@ export class GristDoc extends DisposableWithEvents {
await commands.allCommands.rightPanelOpen.run();
const editLayoutButton = document.querySelector('.behavioral-prompt-edit-card-layout');
if (!editLayoutButton) { throw new Error('GristDoc failed to find edit card layout button'); }
if (!editLayoutButton) {
throw new Error('GristDoc failed to find edit card layout button');
}
this.behavioralPromptsManager.showTip(editLayoutButton, 'editCardLayout', {
popupOptions: {
@@ -1424,7 +1498,7 @@ export class GristDoc extends DisposableWithEvents {
* Helper called before an action is sent to the server. It saves cursor position to come back to
* in case of Undo.
*/
private _onSendActionsStart(ev: {cursorPos: CursorPos}) {
private _onSendActionsStart(ev: { cursorPos: CursorPos }) {
this._lastOwnActionGroup = null;
ev.cursorPos = this.getCursorPos();
}
@@ -1433,7 +1507,7 @@ export class GristDoc extends DisposableWithEvents {
* Helper called when server responds to an action. It attaches the saved cursor position to the
* received action (if any), and stores also the resulting position.
*/
private _onSendActionsEnd(ev: {cursorPos: CursorPos}) {
private _onSendActionsEnd(ev: { cursorPos: CursorPos }) {
const a = this._lastOwnActionGroup;
if (a) {
a.cursorPos = ev.cursorPos;
@@ -1445,15 +1519,15 @@ export class GristDoc extends DisposableWithEvents {
private _getDocApiDownloadParams() {
const filters = this.viewModel.activeSection.peek().activeFilters.get().map(filterInfo => ({
colRef : filterInfo.fieldOrColumn.origCol().origColRef(),
filter : filterInfo.filter()
colRef: filterInfo.fieldOrColumn.origCol().origColRef(),
filter: filterInfo.filter()
}));
const params = {
viewSection: this.viewModel.activeSectionId(),
tableId: this.viewModel.activeSection().table().tableId(),
activeSortSpec: JSON.stringify(this.viewModel.activeSection().activeSortSpec()),
filters : JSON.stringify(filters),
filters: JSON.stringify(filters),
};
return params;
}
@@ -1485,10 +1559,14 @@ export class GristDoc extends DisposableWithEvents {
private async _getTableData(section: ViewSectionRec): Promise<TableData> {
const viewInstance = await waitObs(section.viewInstance);
if (!viewInstance) { throw new Error('view not found'); }
if (!viewInstance) {
throw new Error('view not found');
}
await viewInstance.getLoadingDonePromise();
const table = this.docData.getTable(section.table.peek().tableId.peek());
if (!table) { throw new Error('no section table'); }
if (!table) {
throw new Error('no section table');
}
return table;
}
@@ -1496,13 +1574,15 @@ export class GristDoc extends DisposableWithEvents {
* Convert a url hash to a cursor position.
*/
private _getCursorPosFromHash(hash: HashLink): CursorPos {
const cursorPos: CursorPos = { rowId: hash.rowId, sectionId: hash.sectionId };
if (cursorPos.sectionId != undefined && hash.colRef !== undefined){
const cursorPos: CursorPos = {rowId: hash.rowId, sectionId: hash.sectionId};
if (cursorPos.sectionId != undefined && hash.colRef !== undefined) {
// translate colRef to a fieldIndex
const section = this.docModel.viewSections.getRowModel(cursorPos.sectionId);
const fieldIndex = section.viewFields.peek().all()
.findIndex(x=> x.colRef.peek() == hash.colRef);
if (fieldIndex >= 0) { cursorPos.fieldIndex = fieldIndex; }
.findIndex(x => x.colRef.peek() == hash.colRef);
if (fieldIndex >= 0) {
cursorPos.fieldIndex = fieldIndex;
}
}
return cursorPos;
}
@@ -1556,11 +1636,13 @@ export class GristDoc extends DisposableWithEvents {
const viewFields = viewSection.viewFields.peek().peek();
// If no y-series, then simply return.
if (viewFields.length === 1) { return; }
if (viewFields.length === 1) {
return;
}
const field = viewSection.viewFields.peek().peek()[1];
if (isNumericOnly(viewSection.chartTypeDef.peek()) &&
!isNumericLike(field.column.peek())) {
!isNumericLike(field.column.peek())) {
const actions: UserAction[] = [];
// remove non-numeric field
@@ -1580,10 +1662,28 @@ export class GristDoc extends DisposableWithEvents {
await this.docModel.viewFields.sendTableActions(actions);
}
}
private _handleTriggerQueueOverflowMessage() {
this.listenTo(this, 'webhookOverflowError', (err: any) => {
this.app.topAppModel.notifier.createNotification({
message: err.toString(),
canUserClose: false,
level: "error",
badgeCounter: true,
expireSec: 5,
key: 'webhookOverflowError',
actions: [{
label: t('go to webhook settings'), action: async () => {
await urlState().pushUrl({docPage: 'webhook'});
}
}]
});
});
}
}
async function finalizeAnchor() {
await urlState().pushUrl({ hash: {} }, { replace: true });
await urlState().pushUrl({hash: {}}, {replace: true});
setTestState({anchorApplied: true});
}