mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Add additional telemetry events
Summary: The new events capture usage of forms, widgets, access rules, and onboarding tours and tips. Test Plan: Manual. Reviewers: dsagal Reviewed By: dsagal Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D4189
This commit is contained in:
parent
7f9e2817d1
commit
b8f32d1784
@ -8,6 +8,7 @@ import {aclSelect} from 'app/client/aclui/ACLSelect';
|
|||||||
import {ACLUsersPopup} from 'app/client/aclui/ACLUsers';
|
import {ACLUsersPopup} from 'app/client/aclui/ACLUsers';
|
||||||
import {PermissionKey, permissionsWidget} from 'app/client/aclui/PermissionsWidget';
|
import {PermissionKey, permissionsWidget} from 'app/client/aclui/PermissionsWidget';
|
||||||
import {GristDoc} from 'app/client/components/GristDoc';
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
|
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||||
import {reportError, UserError} from 'app/client/models/errors';
|
import {reportError, UserError} from 'app/client/models/errors';
|
||||||
import {TableData} from 'app/client/models/TableData';
|
import {TableData} from 'app/client/models/TableData';
|
||||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
||||||
@ -308,6 +309,13 @@ export class AccessRules extends Disposable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logTelemetryEvent('changedAccessRules', {
|
||||||
|
full: {
|
||||||
|
docIdDigest: this.gristDoc.docId(),
|
||||||
|
ruleCount: newRules.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// We need to fill in rulePos values. We'll add them in the order the rules are listed (since
|
// We need to fill in rulePos values. We'll add them in the order the rules are listed (since
|
||||||
// this.getRules() returns them in a suitable order), keeping rulePos unchanged when possible.
|
// this.getRules() returns them in a suitable order), keeping rulePos unchanged when possible.
|
||||||
let lastGoodRulePos = 0;
|
let lastGoodRulePos = 0;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {showBehavioralPrompt} from 'app/client/components/modals';
|
import {showBehavioralPrompt} from 'app/client/components/modals';
|
||||||
|
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||||
import {AppModel} from 'app/client/models/AppModel';
|
import {AppModel} from 'app/client/models/AppModel';
|
||||||
import {getUserPrefObs} from 'app/client/models/UserPrefs';
|
import {getUserPrefObs} from 'app/client/models/UserPrefs';
|
||||||
import {GristBehavioralPrompts} from 'app/client/ui/GristTooltips';
|
import {GristBehavioralPrompts} from 'app/client/ui/GristTooltips';
|
||||||
@ -159,6 +160,8 @@ export class BehavioralPromptsManager extends Disposable {
|
|||||||
});
|
});
|
||||||
dom.onElem(refElement, 'click', () => close());
|
dom.onElem(refElement, 'click', () => close());
|
||||||
dom.onDisposeElem(refElement, () => close());
|
dom.onDisposeElem(refElement, () => close());
|
||||||
|
|
||||||
|
logTelemetryEvent('viewedTip', {full: {tipName: prompt}});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _showNextQueuedTip() {
|
private _showNextQueuedTip() {
|
||||||
|
@ -2,7 +2,7 @@ import {cssBannerLink} from 'app/client/components/Banner';
|
|||||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||||
import {urlState} from 'app/client/models/gristUrlState';
|
import {urlState} from 'app/client/models/gristUrlState';
|
||||||
import {docListHeader} from 'app/client/ui/DocMenuCss';
|
import {docListHeader} from 'app/client/ui/DocMenuCss';
|
||||||
import {GristTooltips, TooltipContentFunc} from 'app/client/ui/GristTooltips';
|
import {Tooltip} from 'app/client/ui/GristTooltips';
|
||||||
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
||||||
import {mediaXSmall, theme} from 'app/client/ui2018/cssVars';
|
import {mediaXSmall, theme} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
@ -81,7 +81,7 @@ export class DocumentUsage extends Disposable {
|
|||||||
maximumValue: maxValue ?? DEFAULT_MAX_DATA_SIZE,
|
maximumValue: maxValue ?? DEFAULT_MAX_DATA_SIZE,
|
||||||
unit: 'MB',
|
unit: 'MB',
|
||||||
shouldHideLimits: maxValue === undefined,
|
shouldHideLimits: maxValue === undefined,
|
||||||
tooltipContentFunc: GristTooltips.dataSize,
|
tooltip: 'dataSize',
|
||||||
formatValue: (val) => {
|
formatValue: (val) => {
|
||||||
// To display a nice, round number for `maximumValue`, we first convert
|
// To display a nice, round number for `maximumValue`, we first convert
|
||||||
// to KiBs (base-2), and then convert to MBs (base-10). Normally, we wouldn't
|
// to KiBs (base-2), and then convert to MBs (base-10). Normally, we wouldn't
|
||||||
@ -271,7 +271,7 @@ interface MetricOptions {
|
|||||||
// If true, limits will always be hidden, even if `maximumValue` is a positive number.
|
// If true, limits will always be hidden, even if `maximumValue` is a positive number.
|
||||||
shouldHideLimits?: boolean;
|
shouldHideLimits?: boolean;
|
||||||
// Shows an icon next to the metric name that displays a tooltip on hover.
|
// Shows an icon next to the metric name that displays a tooltip on hover.
|
||||||
tooltipContentFunc?: TooltipContentFunc;
|
tooltip?: Tooltip;
|
||||||
formatValue?(value: number): string;
|
formatValue?(value: number): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,14 +281,11 @@ interface MetricOptions {
|
|||||||
* close `currentValue` is to hitting `maximumValue`.
|
* close `currentValue` is to hitting `maximumValue`.
|
||||||
*/
|
*/
|
||||||
function buildUsageMetric(options: MetricOptions, ...domArgs: DomElementArg[]) {
|
function buildUsageMetric(options: MetricOptions, ...domArgs: DomElementArg[]) {
|
||||||
const {name, tooltipContentFunc} = options;
|
const {name, tooltip} = options;
|
||||||
return cssUsageMetric(
|
return cssUsageMetric(
|
||||||
cssMetricName(
|
cssMetricName(
|
||||||
tooltipContentFunc
|
tooltip
|
||||||
? withInfoTooltip(
|
? withInfoTooltip(cssOverflowableText(name, testId('name')), tooltip)
|
||||||
cssOverflowableText(name, testId('name')),
|
|
||||||
tooltipContentFunc()
|
|
||||||
)
|
|
||||||
: cssOverflowableText(name, testId('name')),
|
: cssOverflowableText(name, testId('name')),
|
||||||
),
|
),
|
||||||
buildUsageProgressBar(options),
|
buildUsageProgressBar(options),
|
||||||
|
@ -11,6 +11,7 @@ import {Disposable} from 'app/client/lib/dispose';
|
|||||||
import {AsyncComputed, makeTestId, stopEvent} from 'app/client/lib/domUtils';
|
import {AsyncComputed, makeTestId, stopEvent} from 'app/client/lib/domUtils';
|
||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
|
import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
|
||||||
|
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||||
import DataTableModel from 'app/client/models/DataTableModel';
|
import DataTableModel from 'app/client/models/DataTableModel';
|
||||||
import {ViewFieldRec, ViewSectionRec} from 'app/client/models/DocModel';
|
import {ViewFieldRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
import {ShareRec} from 'app/client/models/entities/ShareRec';
|
import {ShareRec} from 'app/client/models/entities/ShareRec';
|
||||||
@ -514,6 +515,13 @@ export class FormView extends Disposable {
|
|||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logTelemetryEvent('publishedForm', {
|
||||||
|
full: {
|
||||||
|
docIdDigest: this.gristDoc.docId(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await this.gristDoc.docModel.docData.bundleActions('Publish form', async () => {
|
await this.gristDoc.docModel.docData.bundleActions('Publish form', async () => {
|
||||||
if (!validShare) {
|
if (!validShare) {
|
||||||
const shareRef = await this.gristDoc.docModel.docData.sendAction([
|
const shareRef = await this.gristDoc.docModel.docData.sendAction([
|
||||||
@ -573,6 +581,12 @@ export class FormView extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _unpublishForm() {
|
private async _unpublishForm() {
|
||||||
|
logTelemetryEvent('unpublishedForm', {
|
||||||
|
full: {
|
||||||
|
docIdDigest: this.gristDoc.docId(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await this.gristDoc.docModel.docData.bundleActions('Unpublish form', async () => {
|
await this.gristDoc.docModel.docData.bundleActions('Unpublish form', async () => {
|
||||||
this.viewSection.shareOptionsObj.update({
|
this.viewSection.shareOptionsObj.update({
|
||||||
publish: false,
|
publish: false,
|
||||||
|
@ -47,9 +47,10 @@ import {DocTutorial} from 'app/client/ui/DocTutorial';
|
|||||||
import {DocSettingsPage} from 'app/client/ui/DocumentSettings';
|
import {DocSettingsPage} from 'app/client/ui/DocumentSettings';
|
||||||
import {isTourActive} from "app/client/ui/OnBoardingPopups";
|
import {isTourActive} from "app/client/ui/OnBoardingPopups";
|
||||||
import {DefaultPageWidget, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
|
import {DefaultPageWidget, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
|
||||||
import {linkFromId, selectBy} from 'app/client/ui/selectBy';
|
import {linkFromId, NoLink, selectBy} from 'app/client/ui/selectBy';
|
||||||
import {WebhookPage} from 'app/client/ui/WebhookPage';
|
import {WebhookPage} from 'app/client/ui/WebhookPage';
|
||||||
import {startWelcomeTour} from 'app/client/ui/WelcomeTour';
|
import {startWelcomeTour} from 'app/client/ui/WelcomeTour';
|
||||||
|
import {getTelemetryWidgetTypeFromPageWidget} from 'app/client/ui/widgetTypesMap';
|
||||||
import {PlayerState, YouTubePlayer} from 'app/client/ui/YouTubePlayer';
|
import {PlayerState, YouTubePlayer} from 'app/client/ui/YouTubePlayer';
|
||||||
import {isNarrowScreen, mediaSmall, mediaXSmall, testId, theme} from 'app/client/ui2018/cssVars';
|
import {isNarrowScreen, mediaSmall, mediaXSmall, testId, theme} from 'app/client/ui2018/cssVars';
|
||||||
import {IconName} from 'app/client/ui2018/IconList';
|
import {IconName} from 'app/client/ui2018/IconList';
|
||||||
@ -882,6 +883,13 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const widgetType = getTelemetryWidgetTypeFromPageWidget(val);
|
||||||
|
logTelemetryEvent('addedWidget', {full: {docIdDigest: this.docId(), widgetType}});
|
||||||
|
if (val.link !== NoLink) {
|
||||||
|
logTelemetryEvent('linkedWidget', {full: {docIdDigest: this.docId(), widgetType}});
|
||||||
|
}
|
||||||
|
|
||||||
const res: {sectionRef: number} = await docData.bundleActions(
|
const res: {sectionRef: number} = await docData.bundleActions(
|
||||||
t("Added new linked section to view {{viewName}}", {viewName}),
|
t("Added new linked section to view {{viewName}}", {viewName}),
|
||||||
() => this.addWidgetToPageImpl(val, tableId ?? null)
|
() => this.addWidgetToPageImpl(val, tableId ?? null)
|
||||||
@ -932,6 +940,14 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
* Adds a new page (aka: view) with a single view section (aka: page widget) described by `val`.
|
* Adds a new page (aka: view) with a single view section (aka: page widget) described by `val`.
|
||||||
*/
|
*/
|
||||||
public async addNewPage(val: IPageWidget) {
|
public async addNewPage(val: IPageWidget) {
|
||||||
|
logTelemetryEvent('addedPage', {full: {docIdDigest: this.docId()}});
|
||||||
|
logTelemetryEvent('addedWidget', {
|
||||||
|
full: {
|
||||||
|
docIdDigest: this.docId(),
|
||||||
|
widgetType: getTelemetryWidgetTypeFromPageWidget(val),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (val.table === 'New Table') {
|
if (val.table === 'New Table') {
|
||||||
const name = await this._promptForName();
|
const name = await this._promptForName();
|
||||||
if (name === undefined) {
|
if (name === undefined) {
|
||||||
|
@ -4,12 +4,14 @@ import {DocumentUsage} from 'app/client/components/DocumentUsage';
|
|||||||
import {GristDoc} from 'app/client/components/GristDoc';
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
import {printViewSection} from 'app/client/components/Printing';
|
import {printViewSection} from 'app/client/components/Printing';
|
||||||
import {ViewSectionHelper} from 'app/client/components/ViewLayout';
|
import {ViewSectionHelper} from 'app/client/components/ViewLayout';
|
||||||
|
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||||
import {mediaSmall, theme, vars} from 'app/client/ui2018/cssVars';
|
import {mediaSmall, theme, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {Computed, Disposable, dom, fromKo, makeTestId, Observable, styled} from 'grainjs';
|
import {Computed, Disposable, dom, fromKo, makeTestId, Observable, styled} from 'grainjs';
|
||||||
import {reportError} from 'app/client/models/errors';
|
import {reportError} from 'app/client/models/errors';
|
||||||
import {ViewSectionRec} from 'app/client/models/DocModel';
|
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
import {buildViewSectionDom} from 'app/client/components/buildViewSectionDom';
|
import {buildViewSectionDom} from 'app/client/components/buildViewSectionDom';
|
||||||
|
import {getTelemetryWidgetTypeFromVS} from 'app/client/ui/widgetTypesMap';
|
||||||
|
|
||||||
const testId = makeTestId('test-raw-data-');
|
const testId = makeTestId('test-raw-data-');
|
||||||
|
|
||||||
@ -82,6 +84,10 @@ export class RawDataPopup extends Disposable {
|
|||||||
if (this._viewSection.isRaw.peek()) {
|
if (this._viewSection.isRaw.peek()) {
|
||||||
throw new Error("Can't delete a raw section");
|
throw new Error("Can't delete a raw section");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const widgetType = getTelemetryWidgetTypeFromVS(this._viewSection);
|
||||||
|
logTelemetryEvent('deletedWidget', {full: {docIdDigest: this._gristDoc.docId(), widgetType}});
|
||||||
|
|
||||||
this._gristDoc.docData.sendAction(['RemoveViewSection', this._viewSection.id.peek()]).catch(reportError);
|
this._gristDoc.docData.sendAction(['RemoveViewSection', this._viewSection.id.peek()]).catch(reportError);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -14,8 +14,10 @@ import {LayoutTray} from 'app/client/components/LayoutTray';
|
|||||||
import {printViewSection} from 'app/client/components/Printing';
|
import {printViewSection} from 'app/client/components/Printing';
|
||||||
import {Delay} from 'app/client/lib/Delay';
|
import {Delay} from 'app/client/lib/Delay';
|
||||||
import {createObsArray} from 'app/client/lib/koArrayWrap';
|
import {createObsArray} from 'app/client/lib/koArrayWrap';
|
||||||
|
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||||
import {ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
import {ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
import {reportError} from 'app/client/models/errors';
|
import {reportError} from 'app/client/models/errors';
|
||||||
|
import {getTelemetryWidgetTypeFromVS} from 'app/client/ui/widgetTypesMap';
|
||||||
import {isNarrowScreen, mediaSmall, testId, theme} from 'app/client/ui2018/cssVars';
|
import {isNarrowScreen, mediaSmall, testId, theme} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||||||
@ -279,6 +281,14 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent {
|
|||||||
// more than one viewsection in the view.
|
// more than one viewsection in the view.
|
||||||
public removeViewSection(viewSectionRowId: number) {
|
public removeViewSection(viewSectionRowId: number) {
|
||||||
this.maximized.set(null);
|
this.maximized.set(null);
|
||||||
|
const viewSection = this.viewModel.viewSections().all().find(s => s.getRowId() === viewSectionRowId);
|
||||||
|
if (!viewSection) {
|
||||||
|
throw new Error(`Section not found: ${viewSectionRowId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgetType = getTelemetryWidgetTypeFromVS(viewSection);
|
||||||
|
logTelemetryEvent('deletedWidget', {full: {docIdDigest: this.gristDoc.docId(), widgetType}});
|
||||||
|
|
||||||
this.gristDoc.docData.sendAction(['RemoveViewSection', viewSectionRowId]).catch(reportError);
|
this.gristDoc.docData.sendAction(['RemoveViewSection', viewSectionRowId]).catch(reportError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { GristDoc } from 'app/client/components/GristDoc';
|
import { GristDoc } from 'app/client/components/GristDoc';
|
||||||
|
import { logTelemetryEvent } from 'app/client/lib/telemetry';
|
||||||
import { ViewFieldRec, ViewSectionRec } from 'app/client/models/DocModel';
|
import { ViewFieldRec, ViewSectionRec } from 'app/client/models/DocModel';
|
||||||
import { cssInput } from 'app/client/ui/cssInput';
|
import { cssInput } from 'app/client/ui/cssInput';
|
||||||
import { cssField, cssLabel } from 'app/client/ui/MakeCopyMenu';
|
import { cssField, cssLabel } from 'app/client/ui/MakeCopyMenu';
|
||||||
@ -43,6 +44,8 @@ async function makeDuplicate(gristDoc: GristDoc, pageId: number, pageName: strin
|
|||||||
await gristDoc.docData.bundleActions(
|
await gristDoc.docData.bundleActions(
|
||||||
t("Duplicate page {{pageName}}", {pageName}),
|
t("Duplicate page {{pageName}}", {pageName}),
|
||||||
async () => {
|
async () => {
|
||||||
|
logTelemetryEvent('addedPage', {full: {docIdDigest: gristDoc.docId()}});
|
||||||
|
|
||||||
// create new view and new sections
|
// create new view and new sections
|
||||||
const results = await createNewViewSections(gristDoc.docData, viewSections);
|
const results = await createNewViewSections(gristDoc.docData, viewSections);
|
||||||
viewRef = results[0].viewRef;
|
viewRef = results[0].viewRef;
|
||||||
|
@ -2,7 +2,6 @@ import {makeT} from 'app/client/lib/localization';
|
|||||||
import {GristDoc} from 'app/client/components/GristDoc';
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
import {BEHAVIOR, ColumnRec} from 'app/client/models/entities/ColumnRec';
|
import {BEHAVIOR, ColumnRec} from 'app/client/models/entities/ColumnRec';
|
||||||
import {buildHighlightedCode, cssCodeBlock} from 'app/client/ui/CodeHighlight';
|
import {buildHighlightedCode, cssCodeBlock} from 'app/client/ui/CodeHighlight';
|
||||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
|
||||||
import {cssBlockedCursor, cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
|
import {cssBlockedCursor, cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
|
||||||
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
||||||
import {buildFormulaTriggers} from 'app/client/ui/TriggerFormulas';
|
import {buildFormulaTriggers} from 'app/client/ui/TriggerFormulas';
|
||||||
@ -359,7 +358,7 @@ export function buildFormulaConfig(
|
|||||||
dom.prop("disabled", use => use(isSummaryTable) || use(disableOtherActions)),
|
dom.prop("disabled", use => use(isSummaryTable) || use(disableOtherActions)),
|
||||||
testId("field-set-trigger")
|
testId("field-set-trigger")
|
||||||
),
|
),
|
||||||
GristTooltips.setTriggerFormula(),
|
'setTriggerFormula',
|
||||||
)),
|
)),
|
||||||
cssRow(textButton(
|
cssRow(textButton(
|
||||||
t("Make into data column"),
|
t("Make into data column"),
|
||||||
@ -412,7 +411,7 @@ export function buildFormulaConfig(
|
|||||||
dom.prop("disabled", disableOtherActions),
|
dom.prop("disabled", disableOtherActions),
|
||||||
testId("field-set-trigger")
|
testId("field-set-trigger")
|
||||||
),
|
),
|
||||||
GristTooltips.setTriggerFormula()
|
'setTriggerFormula'
|
||||||
)),
|
)),
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
|
@ -4,7 +4,6 @@ import GridView from 'app/client/components/GridView';
|
|||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
import {ColumnRec} from "app/client/models/entities/ColumnRec";
|
import {ColumnRec} from "app/client/models/entities/ColumnRec";
|
||||||
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
|
||||||
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
||||||
import {isNarrowScreen, testId, theme, vars} from 'app/client/ui2018/cssVars';
|
import {isNarrowScreen, testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {IconName} from "app/client/ui2018/IconList";
|
import {IconName} from "app/client/ui2018/IconList";
|
||||||
@ -136,7 +135,7 @@ function buildAddNewColumMenuSection(gridView: GridView, index?: number): DomEle
|
|||||||
},
|
},
|
||||||
withInfoTooltip(
|
withInfoTooltip(
|
||||||
t('Add formula column'),
|
t('Add formula column'),
|
||||||
GristTooltips.formulaColumn(),
|
'formulaColumn',
|
||||||
{variant: 'hover'}
|
{variant: 'hover'}
|
||||||
),
|
),
|
||||||
testId('new-columns-menu-add-formula'),
|
testId('new-columns-menu-add-formula'),
|
||||||
@ -385,7 +384,7 @@ function buildUUIDMenuItem(gridView: GridView, index?: number) {
|
|||||||
},
|
},
|
||||||
withInfoTooltip(
|
withInfoTooltip(
|
||||||
t('UUID'),
|
t('UUID'),
|
||||||
GristTooltips.uuid(),
|
'uuid',
|
||||||
{variant: 'hover'}
|
{variant: 'hover'}
|
||||||
),
|
),
|
||||||
testId('new-columns-menu-shortcuts-uuid'),
|
testId('new-columns-menu-shortcuts-uuid'),
|
||||||
@ -680,7 +679,7 @@ function buildLookupSection(gridView: GridView, index?: number){
|
|||||||
menuSubHeader(
|
menuSubHeader(
|
||||||
withInfoTooltip(
|
withInfoTooltip(
|
||||||
t('Lookups'),
|
t('Lookups'),
|
||||||
GristTooltips.lookups(),
|
'lookups',
|
||||||
{variant: 'hover'}
|
{variant: 'hover'}
|
||||||
),
|
),
|
||||||
testId('new-columns-menu-lookups'),
|
testId('new-columns-menu-lookups'),
|
||||||
|
@ -38,7 +38,7 @@ export type Tooltip =
|
|||||||
| 'addColumnConditionalStyle'
|
| 'addColumnConditionalStyle'
|
||||||
| 'uuid'
|
| 'uuid'
|
||||||
| 'lookups'
|
| 'lookups'
|
||||||
| 'formulaColumn'
|
| 'formulaColumn';
|
||||||
|
|
||||||
export type TooltipContentFunc = (...domArgs: DomElementArg[]) => DomContents;
|
export type TooltipContentFunc = (...domArgs: DomElementArg[]) => DomContents;
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ export interface IOnBoardingMsg {
|
|||||||
// starting a new one.
|
// starting a new one.
|
||||||
const tourSingleton = Holder.create<OnBoardingPopupsCtl>(null);
|
const tourSingleton = Holder.create<OnBoardingPopupsCtl>(null);
|
||||||
|
|
||||||
export function startOnBoarding(messages: IOnBoardingMsg[], onFinishCB: () => void) {
|
export function startOnBoarding(messages: IOnBoardingMsg[], onFinishCB: (lastMessageIndex: number) => void) {
|
||||||
const ctl = OnBoardingPopupsCtl.create(tourSingleton, messages, onFinishCB);
|
const ctl = OnBoardingPopupsCtl.create(tourSingleton, messages, onFinishCB);
|
||||||
ctl.start().catch(reportError);
|
ctl.start().catch(reportError);
|
||||||
}
|
}
|
||||||
@ -109,7 +109,7 @@ class OnBoardingPopupsCtl extends Disposable {
|
|||||||
private _overlay: HTMLElement;
|
private _overlay: HTMLElement;
|
||||||
private _arrowEl = buildArrow();
|
private _arrowEl = buildArrow();
|
||||||
|
|
||||||
constructor(private _messages: IOnBoardingMsg[], private _onFinishCB: () => void) {
|
constructor(private _messages: IOnBoardingMsg[], private _onFinishCB: (lastMessageIndex: number) => void) {
|
||||||
super();
|
super();
|
||||||
if (this._messages.length === 0) {
|
if (this._messages.length === 0) {
|
||||||
throw new OnBoardingError('messages should not be an empty list');
|
throw new OnBoardingError('messages should not be an empty list');
|
||||||
@ -133,8 +133,8 @@ class OnBoardingPopupsCtl extends Disposable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _finish() {
|
private _finish(lastMessageIndex: number) {
|
||||||
this._onFinishCB();
|
this._onFinishCB(lastMessageIndex);
|
||||||
this.dispose();
|
this.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,9 +143,9 @@ class OnBoardingPopupsCtl extends Disposable {
|
|||||||
const entry = this._messages[newIndex];
|
const entry = this._messages[newIndex];
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
if (maybeClose) {
|
if (maybeClose) {
|
||||||
|
this._finish(ctlIndex);
|
||||||
// User finished the tour, close and restart from the beginning if they reopen
|
// User finished the tour, close and restart from the beginning if they reopen
|
||||||
ctlIndex = 0;
|
ctlIndex = 0;
|
||||||
this._finish();
|
|
||||||
}
|
}
|
||||||
return; // gone out of bounds, probably by keyboard shortcut
|
return; // gone out of bounds, probably by keyboard shortcut
|
||||||
}
|
}
|
||||||
@ -266,7 +266,7 @@ class OnBoardingPopupsCtl extends Disposable {
|
|||||||
this._arrowEl,
|
this._arrowEl,
|
||||||
ContentWrapper(
|
ContentWrapper(
|
||||||
cssCloseButton(cssBigIcon('CrossBig'),
|
cssCloseButton(cssBigIcon('CrossBig'),
|
||||||
dom.on('click', () => this._finish()),
|
dom.on('click', () => this._finish(ctlIndex)),
|
||||||
testId('close'),
|
testId('close'),
|
||||||
),
|
),
|
||||||
cssTitle(this._messages[ctlIndex].title),
|
cssTitle(this._messages[ctlIndex].title),
|
||||||
@ -275,7 +275,7 @@ class OnBoardingPopupsCtl extends Disposable {
|
|||||||
testId('popup'),
|
testId('popup'),
|
||||||
),
|
),
|
||||||
dom.onKeyDown({
|
dom.onKeyDown({
|
||||||
Escape: () => this._finish(),
|
Escape: () => this._finish(ctlIndex),
|
||||||
ArrowLeft: () => this._move(-1),
|
ArrowLeft: () => this._move(-1),
|
||||||
ArrowRight: () => this._move(+1),
|
ArrowRight: () => this._move(+1),
|
||||||
Enter: () => this._move(+1, true),
|
Enter: () => this._move(+1, true),
|
||||||
|
@ -4,7 +4,6 @@ import {makeT} from 'app/client/lib/localization';
|
|||||||
import {reportError} from 'app/client/models/AppModel';
|
import {reportError} from 'app/client/models/AppModel';
|
||||||
import {ColumnRec, TableRec, ViewSectionRec} from 'app/client/models/DocModel';
|
import {ColumnRec, TableRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
import {PERMITTED_CUSTOM_WIDGETS} from "app/client/models/features";
|
import {PERMITTED_CUSTOM_WIDGETS} from "app/client/models/features";
|
||||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
|
||||||
import {linkId, NoLink} from 'app/client/ui/selectBy';
|
import {linkId, NoLink} from 'app/client/ui/selectBy';
|
||||||
import {overflowTooltip, withInfoTooltip} from 'app/client/ui/tooltips';
|
import {overflowTooltip, withInfoTooltip} from 'app/client/ui/tooltips';
|
||||||
import {getWidgetTypes} from "app/client/ui/widgetTypesMap";
|
import {getWidgetTypes} from "app/client/ui/widgetTypesMap";
|
||||||
@ -394,7 +393,7 @@ export class PageWidgetSelect extends Disposable {
|
|||||||
dom.update(cssSelect(this._value.link, this._selectByOptions!),
|
dom.update(cssSelect(this._value.link, this._selectByOptions!),
|
||||||
testId('selectby'))
|
testId('selectby'))
|
||||||
),
|
),
|
||||||
GristTooltips.selectBy(),
|
'selectBy',
|
||||||
{popupOptions: {attach: null}, domArgs: [
|
{popupOptions: {attach: null}, domArgs: [
|
||||||
this._behavioralPromptsManager.attachTip('pageWidgetPickerSelectBy', {
|
this._behavioralPromptsManager.attachTip('pageWidgetPickerSelectBy', {
|
||||||
popupOptions: {
|
popupOptions: {
|
||||||
|
@ -2,6 +2,7 @@ import {createGroup} from 'app/client/components/commands';
|
|||||||
import {duplicatePage} from 'app/client/components/duplicatePage';
|
import {duplicatePage} from 'app/client/components/duplicatePage';
|
||||||
import {GristDoc} from 'app/client/components/GristDoc';
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||||
import {PageRec} from 'app/client/models/DocModel';
|
import {PageRec} from 'app/client/models/DocModel';
|
||||||
import {urlState} from 'app/client/models/gristUrlState';
|
import {urlState} from 'app/client/models/gristUrlState';
|
||||||
import MetaTableModel from 'app/client/models/MetaTableModel';
|
import MetaTableModel from 'app/client/models/MetaTableModel';
|
||||||
@ -83,6 +84,8 @@ function buildDomFromTable(pagesTable: MetaTableModel<PageRec>, activeDoc: Grist
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeView(activeDoc: GristDoc, viewId: number, pageName: string) {
|
function removeView(activeDoc: GristDoc, viewId: number, pageName: string) {
|
||||||
|
logTelemetryEvent('deletedPage', {full: {docIdDigest: activeDoc.docId()}});
|
||||||
|
|
||||||
const docData = activeDoc.docData;
|
const docData = activeDoc.docData;
|
||||||
// Create a set with tables on other pages (but not on this one).
|
// Create a set with tables on other pages (but not on this one).
|
||||||
const tablesOnOtherViews = new Set(activeDoc.docModel.viewSections.rowModels
|
const tablesOnOtherViews = new Set(activeDoc.docModel.viewSections.rowModels
|
||||||
|
@ -25,6 +25,7 @@ import {domAsync} from 'app/client/lib/domAsync';
|
|||||||
import * as imports from 'app/client/lib/imports';
|
import * as imports from 'app/client/lib/imports';
|
||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
import {createSessionObs, isBoolean, SessionObs} from 'app/client/lib/sessionObs';
|
import {createSessionObs, isBoolean, SessionObs} from 'app/client/lib/sessionObs';
|
||||||
|
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||||
import {reportError} from 'app/client/models/AppModel';
|
import {reportError} from 'app/client/models/AppModel';
|
||||||
import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel';
|
import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||||
import {CustomSectionConfig} from 'app/client/ui/CustomSectionConfig';
|
import {CustomSectionConfig} from 'app/client/ui/CustomSectionConfig';
|
||||||
@ -35,9 +36,9 @@ import {textarea} from 'app/client/ui/inputs';
|
|||||||
import {attachPageWidgetPicker, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
|
import {attachPageWidgetPicker, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
|
||||||
import {PredefinedCustomSectionConfig} from "app/client/ui/PredefinedCustomSectionConfig";
|
import {PredefinedCustomSectionConfig} from "app/client/ui/PredefinedCustomSectionConfig";
|
||||||
import {cssLabel} from 'app/client/ui/RightPanelStyles';
|
import {cssLabel} from 'app/client/ui/RightPanelStyles';
|
||||||
import {linkId, selectBy} from 'app/client/ui/selectBy';
|
import {linkId, NoLink, selectBy} from 'app/client/ui/selectBy';
|
||||||
import {VisibleFieldsConfig} from 'app/client/ui/VisibleFieldsConfig';
|
import {VisibleFieldsConfig} from 'app/client/ui/VisibleFieldsConfig';
|
||||||
import {widgetTypesMap} from "app/client/ui/widgetTypesMap";
|
import {getTelemetryWidgetTypeFromVS, widgetTypesMap} from "app/client/ui/widgetTypesMap";
|
||||||
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
||||||
import {buttonSelect} from 'app/client/ui2018/buttonSelect';
|
import {buttonSelect} from 'app/client/ui2018/buttonSelect';
|
||||||
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
||||||
@ -792,7 +793,16 @@ export class RightPanel extends Disposable {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
link.onWrite((val) => this._gristDoc.saveLink(val));
|
link.onWrite(async (val) => {
|
||||||
|
const widgetType = getTelemetryWidgetTypeFromVS(activeSection);
|
||||||
|
if (val !== NoLink) {
|
||||||
|
logTelemetryEvent('linkedWidget', {full: {docIdDigest: this._gristDoc.docId(), widgetType}});
|
||||||
|
} else {
|
||||||
|
logTelemetryEvent('unlinkedWidget', {full: {docIdDigest: this._gristDoc.docId(), widgetType}});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._gristDoc.saveLink(val);
|
||||||
|
});
|
||||||
return [
|
return [
|
||||||
this._disableIfReadonly(),
|
this._disableIfReadonly(),
|
||||||
cssLabel(t("DATA TABLE")),
|
cssLabel(t("DATA TABLE")),
|
||||||
|
@ -3,7 +3,6 @@ import {loadUserManager} from 'app/client/lib/imports';
|
|||||||
import {AppModel, reportError} from 'app/client/models/AppModel';
|
import {AppModel, reportError} from 'app/client/models/AppModel';
|
||||||
import {DocInfo, DocPageModel} from 'app/client/models/DocPageModel';
|
import {DocInfo, DocPageModel} from 'app/client/models/DocPageModel';
|
||||||
import {docUrl, getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
import {docUrl, getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
||||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
|
||||||
import {downloadDocModal, makeCopy, replaceTrunkWithFork} from 'app/client/ui/MakeCopyMenu';
|
import {downloadDocModal, makeCopy, replaceTrunkWithFork} from 'app/client/ui/MakeCopyMenu';
|
||||||
import {sendToDrive} from 'app/client/ui/sendToDrive';
|
import {sendToDrive} from 'app/client/ui/sendToDrive';
|
||||||
import {hoverTooltip, withInfoTooltip} from 'app/client/ui/tooltips';
|
import {hoverTooltip, withInfoTooltip} from 'app/client/ui/tooltips';
|
||||||
@ -255,7 +254,7 @@ function menuWorkOnCopy(pageModel: DocPageModel) {
|
|||||||
menuText(
|
menuText(
|
||||||
withInfoTooltip(
|
withInfoTooltip(
|
||||||
t("Edit without affecting the original"),
|
t("Edit without affecting the original"),
|
||||||
GristTooltips.workOnACopy(),
|
'workOnACopy',
|
||||||
{popupOptions: {attach: null}}
|
{popupOptions: {attach: null}}
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -27,7 +27,6 @@ import {IEditableMember, IMemberSelectOption, IOrgMemberSelectOption,
|
|||||||
Resource} from 'app/client/models/UserManagerModel';
|
Resource} from 'app/client/models/UserManagerModel';
|
||||||
import {UserManagerModel, UserManagerModelImpl} from 'app/client/models/UserManagerModel';
|
import {UserManagerModel, UserManagerModelImpl} from 'app/client/models/UserManagerModel';
|
||||||
import {getResourceParent, ResourceType} from 'app/client/models/UserManagerModel';
|
import {getResourceParent, ResourceType} from 'app/client/models/UserManagerModel';
|
||||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
|
||||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
||||||
import {hoverTooltip, ITooltipControl, showTransientTooltip, withInfoTooltip} from 'app/client/ui/tooltips';
|
import {hoverTooltip, ITooltipControl, showTransientTooltip, withInfoTooltip} from 'app/client/ui/tooltips';
|
||||||
import {createUserImage} from 'app/client/ui/UserImage';
|
import {createUserImage} from 'app/client/ui/UserImage';
|
||||||
@ -187,7 +186,7 @@ function buildUserManagerModal(
|
|||||||
}),
|
}),
|
||||||
testId('um-open-access-rules'),
|
testId('um-open-access-rules'),
|
||||||
),
|
),
|
||||||
GristTooltips.openAccessRules(),
|
'openAccessRules',
|
||||||
{domArgs: [cssAccessLink.cls('')]},
|
{domArgs: [cssAccessLink.cls('')]},
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { makeT } from 'app/client/lib/localization';
|
import { makeT } from 'app/client/lib/localization';
|
||||||
|
import { logTelemetryEvent } from 'app/client/lib/telemetry';
|
||||||
import * as commands from 'app/client/components/commands';
|
import * as commands from 'app/client/components/commands';
|
||||||
import { urlState } from 'app/client/models/gristUrlState';
|
import { urlState } from 'app/client/models/gristUrlState';
|
||||||
import { IOnBoardingMsg, startOnBoarding } from "app/client/ui/OnBoardingPopups";
|
import { IOnBoardingMsg, startOnBoarding } from "app/client/ui/OnBoardingPopups";
|
||||||
@ -100,7 +101,15 @@ export function getOnBoardingMessages(): IOnBoardingMsg[] {
|
|||||||
|
|
||||||
export function startWelcomeTour(onFinishCB: () => void) {
|
export function startWelcomeTour(onFinishCB: () => void) {
|
||||||
commands.allCommands.fieldTabOpen.run();
|
commands.allCommands.fieldTabOpen.run();
|
||||||
startOnBoarding(getOnBoardingMessages(), onFinishCB);
|
const messages = getOnBoardingMessages();
|
||||||
|
startOnBoarding(messages, (lastMessageIndex) => {
|
||||||
|
logTelemetryEvent('viewedWelcomeTour', {
|
||||||
|
full: {
|
||||||
|
percentComplete: Math.floor(((lastMessageIndex + 1) / messages.length) * 100),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
onFinishCB();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const TopBarButtonIcon = styled(icon, `
|
const TopBarButtonIcon = styled(icon, `
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
* - to be shown briefly, as a transient notification next to some action element.
|
* - to be shown briefly, as a transient notification next to some action element.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||||
|
import {GristTooltips, Tooltip} from 'app/client/ui/GristTooltips';
|
||||||
import {prepareForTransition} from 'app/client/ui/transitions';
|
import {prepareForTransition} from 'app/client/ui/transitions';
|
||||||
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
@ -312,6 +314,8 @@ export interface InfoTooltipOptions {
|
|||||||
variant?: InfoTooltipVariant;
|
variant?: InfoTooltipVariant;
|
||||||
/** Only applicable to the `click` variant. */
|
/** Only applicable to the `click` variant. */
|
||||||
popupOptions?: IPopupOptions;
|
popupOptions?: IPopupOptions;
|
||||||
|
/** Only applicable to the `click` variant. */
|
||||||
|
onOpen?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InfoTooltipVariant = 'click' | 'hover';
|
export type InfoTooltipVariant = 'click' | 'hover';
|
||||||
@ -320,33 +324,42 @@ export type InfoTooltipVariant = 'click' | 'hover';
|
|||||||
* Renders an info icon that shows a tooltip with the specified `content`.
|
* Renders an info icon that shows a tooltip with the specified `content`.
|
||||||
*/
|
*/
|
||||||
export function infoTooltip(
|
export function infoTooltip(
|
||||||
content: DomContents,
|
tooltip: Tooltip,
|
||||||
options: InfoTooltipOptions = {},
|
options: InfoTooltipOptions = {},
|
||||||
...domArgs: DomElementArg[]
|
...domArgs: DomElementArg[]
|
||||||
) {
|
) {
|
||||||
const {variant = 'click'} = options;
|
const {variant = 'click'} = options;
|
||||||
|
const content = GristTooltips[tooltip]();
|
||||||
|
const onOpen = () => logTelemetryEvent('viewedTip', {full: {tipName: tooltip}});
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'click': {
|
case 'click': {
|
||||||
const {popupOptions} = options;
|
const {popupOptions} = options;
|
||||||
return buildClickableInfoTooltip(content, popupOptions, domArgs);
|
return buildClickableInfoTooltip(content, {onOpen, popupOptions}, domArgs);
|
||||||
}
|
}
|
||||||
case 'hover': {
|
case 'hover': {
|
||||||
return buildHoverableInfoTooltip(content, domArgs);
|
return buildHoverableInfoTooltip(content, domArgs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickableInfoTooltipOptions {
|
||||||
|
popupOptions?: IPopupOptions;
|
||||||
|
onOpen?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildClickableInfoTooltip(
|
function buildClickableInfoTooltip(
|
||||||
content: DomContents,
|
content: DomContents,
|
||||||
popupOptions?: IPopupOptions,
|
options: ClickableInfoTooltipOptions = {},
|
||||||
...domArgs: DomElementArg[]
|
...domArgs: DomElementArg[]
|
||||||
) {
|
) {
|
||||||
|
const {onOpen, popupOptions} = options;
|
||||||
return cssInfoTooltipButton('?',
|
return cssInfoTooltipButton('?',
|
||||||
(elem) => {
|
(elem) => {
|
||||||
setPopupToCreateDom(
|
setPopupToCreateDom(
|
||||||
elem,
|
elem,
|
||||||
(ctl) => {
|
(ctl) => {
|
||||||
|
onOpen?.();
|
||||||
|
|
||||||
return cssInfoTooltipPopup(
|
return cssInfoTooltipPopup(
|
||||||
cssInfoTooltipPopupCloseButton(
|
cssInfoTooltipPopupCloseButton(
|
||||||
icon('CrossSmall'),
|
icon('CrossSmall'),
|
||||||
@ -395,11 +408,13 @@ export interface WithInfoTooltipOptions {
|
|||||||
iconDomArgs?: DomElementArg[];
|
iconDomArgs?: DomElementArg[];
|
||||||
/** Only applicable to the `click` variant. */
|
/** Only applicable to the `click` variant. */
|
||||||
popupOptions?: IPopupOptions;
|
popupOptions?: IPopupOptions;
|
||||||
|
onOpen?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps `domContent` with a info tooltip icon that displays the provided
|
* Wraps `domContent` with a info tooltip icon that displays the specified
|
||||||
* `tooltipContent` and returns the wrapped element.
|
* `tooltip` and returns the wrapped element. Tooltips are defined in
|
||||||
|
* `app/client/ui/GristTooltips.ts`.
|
||||||
*
|
*
|
||||||
* The tooltip button is displayed to the right of `domContents`, and displays
|
* The tooltip button is displayed to the right of `domContents`, and displays
|
||||||
* a popup on click by default. The popup can be dismissed by clicking away from
|
* a popup on click by default. The popup can be dismissed by clicking away from
|
||||||
@ -414,20 +429,17 @@ export interface WithInfoTooltipOptions {
|
|||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
*
|
*
|
||||||
* withInfoTooltip(
|
* withInfoTooltip(dom('div', 'Hello World!'), 'selectBy')
|
||||||
* dom('div', 'Hello World!'),
|
|
||||||
* dom('p', 'This is some text to show inside the tooltip.'),
|
|
||||||
* )
|
|
||||||
*/
|
*/
|
||||||
export function withInfoTooltip(
|
export function withInfoTooltip(
|
||||||
domContents: DomContents,
|
domContents: DomContents,
|
||||||
tooltipContent: DomContents,
|
tooltip: Tooltip,
|
||||||
options: WithInfoTooltipOptions = {},
|
options: WithInfoTooltipOptions = {},
|
||||||
) {
|
) {
|
||||||
const {variant = 'click', domArgs, iconDomArgs, popupOptions} = options;
|
const {variant = 'click', domArgs, iconDomArgs, popupOptions} = options;
|
||||||
return cssDomWithTooltip(
|
return cssDomWithTooltip(
|
||||||
domContents,
|
domContents,
|
||||||
infoTooltip(tooltipContent, {variant, popupOptions}, iconDomArgs),
|
infoTooltip(tooltip, {variant, popupOptions}, iconDomArgs),
|
||||||
...(domArgs ?? [])
|
...(domArgs ?? [])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// the list of widget types with their labels and icons
|
// the list of widget types with their labels and icons
|
||||||
import {IWidgetType} from "app/common/widgetTypes";
|
import {ViewSectionRec} from "app/client/models/entities/ViewSectionRec";
|
||||||
|
import {IPageWidget} from "app/client/ui/PageWidgetPicker";
|
||||||
import {IconName} from "app/client/ui2018/IconList";
|
import {IconName} from "app/client/ui2018/IconList";
|
||||||
|
import {IWidgetType} from "app/common/widgetTypes";
|
||||||
|
|
||||||
export const widgetTypesMap = new Map<IWidgetType, IWidgetTypeInfo>([
|
export const widgetTypesMap = new Map<IWidgetType, IWidgetTypeInfo>([
|
||||||
['record', {label: 'Table', icon: 'TypeTable'}],
|
['record', {label: 'Table', icon: 'TypeTable'}],
|
||||||
@ -22,3 +24,37 @@ export interface IWidgetTypeInfo {
|
|||||||
export function getWidgetTypes(sectionType: IWidgetType | null): IWidgetTypeInfo {
|
export function getWidgetTypes(sectionType: IWidgetType | null): IWidgetTypeInfo {
|
||||||
return widgetTypesMap.get(sectionType || 'record') || widgetTypesMap.get('record')!;
|
return widgetTypesMap.get(sectionType || 'record') || widgetTypesMap.get('record')!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetTelemetryWidgetTypeOptions {
|
||||||
|
/** Defaults to `false`. */
|
||||||
|
isSummary?: boolean;
|
||||||
|
/** Defaults to `false`. */
|
||||||
|
isNewTable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTelemetryWidgetTypeFromVS(vs: ViewSectionRec) {
|
||||||
|
return getTelemetryWidgetType(vs.widgetType.peek(), {
|
||||||
|
isSummary: vs.table.peek().summarySourceTable.peek() !== 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTelemetryWidgetTypeFromPageWidget(widget: IPageWidget) {
|
||||||
|
return getTelemetryWidgetType(widget.type, {
|
||||||
|
isNewTable: widget.table === 'New Table',
|
||||||
|
isSummary: widget.summarize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTelemetryWidgetType(type: IWidgetType, options: GetTelemetryWidgetTypeOptions = {}) {
|
||||||
|
let telemetryWidgetType: string | undefined = widgetTypesMap.get(type)?.label;
|
||||||
|
if (!telemetryWidgetType) { return undefined; }
|
||||||
|
|
||||||
|
if (options.isNewTable) {
|
||||||
|
telemetryWidgetType = 'New ' + telemetryWidgetType;
|
||||||
|
}
|
||||||
|
if (options.isSummary) {
|
||||||
|
telemetryWidgetType += ' (Summary)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return telemetryWidgetType;
|
||||||
|
}
|
||||||
|
@ -5,7 +5,6 @@ import {KoSaveableObservable} from 'app/client/models/modelUtil';
|
|||||||
import {RuleOwner} from 'app/client/models/RuleOwner';
|
import {RuleOwner} from 'app/client/models/RuleOwner';
|
||||||
import {Style} from 'app/client/models/Styles';
|
import {Style} from 'app/client/models/Styles';
|
||||||
import {cssFieldFormula} from 'app/client/ui/FieldConfig';
|
import {cssFieldFormula} from 'app/client/ui/FieldConfig';
|
||||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
|
||||||
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
||||||
import {textButton} from 'app/client/ui2018/buttons';
|
import {textButton} from 'app/client/ui2018/buttons';
|
||||||
import {ColorOption, colorSelect} from 'app/client/ui2018/ColorSelect';
|
import {ColorOption, colorSelect} from 'app/client/ui2018/ColorSelect';
|
||||||
@ -78,10 +77,7 @@ export class ConditionalStyle extends Disposable {
|
|||||||
dom.on('click', () => this._ruleOwner.addEmptyRule()),
|
dom.on('click', () => this._ruleOwner.addEmptyRule()),
|
||||||
dom.prop('disabled', this._disabled),
|
dom.prop('disabled', this._disabled),
|
||||||
),
|
),
|
||||||
(this._label === t('Row Style')
|
this._label === t('Row Style') ? 'addRowConditionalStyle' : 'addColumnConditionalStyle'
|
||||||
? GristTooltips.addRowConditionalStyle()
|
|
||||||
: GristTooltips.addColumnConditionalStyle()
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
dom.hide(use => use(this._ruleOwner.hasRules))
|
dom.hide(use => use(this._ruleOwner.hasRules))
|
||||||
),
|
),
|
||||||
|
@ -252,7 +252,7 @@ export class FormulaAssistant extends Disposable {
|
|||||||
private _logTelemetryEvent(event: TelemetryEvent, includeContext = false, metadata: TelemetryMetadata = {}) {
|
private _logTelemetryEvent(event: TelemetryEvent, includeContext = false, metadata: TelemetryMetadata = {}) {
|
||||||
logTelemetryEvent(event, {
|
logTelemetryEvent(event, {
|
||||||
full: {
|
full: {
|
||||||
docIdDigest: this._gristDoc.docId,
|
docIdDigest: this._gristDoc.docId(),
|
||||||
conversationId: this._chat.conversationId.get(),
|
conversationId: this._chat.conversationId.get(),
|
||||||
...(!includeContext ? {} : {context: {
|
...(!includeContext ? {} : {context: {
|
||||||
type: 'formula',
|
type: 'formula',
|
||||||
|
@ -1428,6 +1428,286 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
viewedWelcomeTour: {
|
||||||
|
category: 'Tutorial',
|
||||||
|
description: 'Triggered when the Grist welcome tour is closed.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
percentComplete: {
|
||||||
|
description: 'Percentage of tour completion.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewedTip: {
|
||||||
|
category: 'Tutorial',
|
||||||
|
description: 'Triggered when a tip is shown.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
tipName: {
|
||||||
|
description: 'The name of the tip.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deletedDoc: {
|
||||||
|
category: 'DocumentUsage',
|
||||||
|
description: 'Triggered when a document is deleted.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
addedPage: {
|
||||||
|
category: 'DocumentUsage',
|
||||||
|
description: 'Triggered when a page is added.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deletedPage: {
|
||||||
|
category: 'DocumentUsage',
|
||||||
|
description: 'Triggered when a page is deleted.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
addedWidget: {
|
||||||
|
category: 'WidgetUsage',
|
||||||
|
description: 'Triggered when a widget is added.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
widgetType: {
|
||||||
|
description: 'The widget type (e.g. "Form").',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deletedWidget: {
|
||||||
|
category: 'WidgetUsage',
|
||||||
|
description: 'Triggered when a widget is deleted.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
widgetType: {
|
||||||
|
description: 'The widget type (e.g. "Form").',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
linkedWidget: {
|
||||||
|
category: 'WidgetUsage',
|
||||||
|
description: 'Triggered when a widget is linked.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
widgetType: {
|
||||||
|
description: 'The widget type (e.g. "Form").',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unlinkedWidget: {
|
||||||
|
category: 'WidgetUsage',
|
||||||
|
description: 'Triggered when a widget is unlinked.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
widgetType: {
|
||||||
|
description: 'The widget type (e.g. "Form").',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
publishedForm: {
|
||||||
|
category: 'WidgetUsage',
|
||||||
|
description: 'Triggered when a form is published.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unpublishedForm: {
|
||||||
|
category: 'WidgetUsage',
|
||||||
|
description: 'Triggered when a form is unpublished.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visitedForm: {
|
||||||
|
category: 'WidgetUsage',
|
||||||
|
description: 'Triggered when a published form is visited.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changedAccessRules: {
|
||||||
|
category: 'AccessRules',
|
||||||
|
description: 'Triggered when a change to access rules is saved.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
ruleCount: {
|
||||||
|
description: 'The number of access rules in the document.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type TelemetryContracts = Record<TelemetryEvent, TelemetryEventContract>;
|
type TelemetryContracts = Record<TelemetryEvent, TelemetryEventContract>;
|
||||||
@ -1484,6 +1764,19 @@ export const TelemetryEvents = StringUnion(
|
|||||||
'createdDoc-FileImport',
|
'createdDoc-FileImport',
|
||||||
'createdDoc-CopyTemplate',
|
'createdDoc-CopyTemplate',
|
||||||
'createdDoc-CopyDoc',
|
'createdDoc-CopyDoc',
|
||||||
|
'viewedWelcomeTour',
|
||||||
|
'viewedTip',
|
||||||
|
'deletedDoc',
|
||||||
|
'addedPage',
|
||||||
|
'deletedPage',
|
||||||
|
'addedWidget',
|
||||||
|
'deletedWidget',
|
||||||
|
'linkedWidget',
|
||||||
|
'unlinkedWidget',
|
||||||
|
'publishedForm',
|
||||||
|
'unpublishedForm',
|
||||||
|
'visitedForm',
|
||||||
|
'changedAccessRules',
|
||||||
);
|
);
|
||||||
export type TelemetryEvent = typeof TelemetryEvents.type;
|
export type TelemetryEvent = typeof TelemetryEvents.type;
|
||||||
|
|
||||||
@ -1496,7 +1789,9 @@ type TelemetryEventCategory =
|
|||||||
| 'SubscriptionPlan'
|
| 'SubscriptionPlan'
|
||||||
| 'DocumentUsage'
|
| 'DocumentUsage'
|
||||||
| 'TeamSite'
|
| 'TeamSite'
|
||||||
| 'ProductVisits';
|
| 'ProductVisits'
|
||||||
|
| 'AccessRules'
|
||||||
|
| 'WidgetUsage';
|
||||||
|
|
||||||
interface TelemetryEventContract {
|
interface TelemetryEventContract {
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -233,7 +233,7 @@ export function attachAppEndpoint(options: AttachOptions): void {
|
|||||||
const response = await fetch(formUrl, {
|
const response = await fetch(formUrl, {
|
||||||
headers: getTransitiveHeaders(req),
|
headers: getTransitiveHeaders(req),
|
||||||
});
|
});
|
||||||
if (response.status === 200) {
|
if (response.ok) {
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
res.send(html);
|
res.send(html);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1540,6 +1540,13 @@ export class DocWorkerApi {
|
|||||||
SUCCESS_URL: redirectUrl,
|
SUCCESS_URL: redirectUrl,
|
||||||
TITLE: `${section.title || tableName || tableId || 'Form'} - Grist`
|
TITLE: `${section.title || tableName || tableId || 'Form'} - Grist`
|
||||||
});
|
});
|
||||||
|
this._grist.getTelemetry().logEvent(req, 'visitedForm', {
|
||||||
|
full: {
|
||||||
|
docIdDigest: activeDoc.docName,
|
||||||
|
userId: req.userId,
|
||||||
|
altSessionId: req.altSessionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
res.status(200).send(renderedHtml);
|
res.status(200).send(renderedHtml);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -2026,6 +2033,7 @@ export class DocWorkerApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _removeDoc(req: Request, res: Response, permanent: boolean) {
|
private async _removeDoc(req: Request, res: Response, permanent: boolean) {
|
||||||
|
const mreq = req as RequestWithLogin;
|
||||||
const scope = getDocScope(req);
|
const scope = getDocScope(req);
|
||||||
const docId = getDocId(req);
|
const docId = getDocId(req);
|
||||||
if (permanent) {
|
if (permanent) {
|
||||||
@ -2045,6 +2053,13 @@ export class DocWorkerApi {
|
|||||||
// Permanently delete from database.
|
// Permanently delete from database.
|
||||||
const query = await this._dbManager.deleteDocument(scope);
|
const query = await this._dbManager.deleteDocument(scope);
|
||||||
this._dbManager.checkQueryResult(query);
|
this._dbManager.checkQueryResult(query);
|
||||||
|
this._grist.getTelemetry().logEvent(mreq, 'deletedDoc', {
|
||||||
|
full: {
|
||||||
|
docIdDigest: docId,
|
||||||
|
userId: mreq.userId,
|
||||||
|
altSessionId: mreq.altSessionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
await sendReply(req, res, query);
|
await sendReply(req, res, query);
|
||||||
} else {
|
} else {
|
||||||
await this._dbManager.softDeleteDocument(scope);
|
await this._dbManager.softDeleteDocument(scope);
|
||||||
|
Loading…
Reference in New Issue
Block a user