mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Forms post-release fixes and improvements
Summary: Fixes misc. bugs with forms, updates Grist URLs on static form pages to link to the new forms marketing page, and adds a forms announcement popup that's shown next to the Add New button within a document. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4185
This commit is contained in:
parent
b8f32d1784
commit
cd339ce7cb
@ -354,7 +354,7 @@ export class AccessRules extends Disposable {
|
|||||||
|
|
||||||
public buildDom() {
|
public buildDom() {
|
||||||
return cssOuter(
|
return cssOuter(
|
||||||
dom('div', this.gristDoc.behavioralPromptsManager.attachTip('accessRules', {
|
dom('div', this.gristDoc.behavioralPromptsManager.attachPopup('accessRules', {
|
||||||
hideArrow: true,
|
hideArrow: true,
|
||||||
})),
|
})),
|
||||||
cssAddTableRow(
|
cssAddTableRow(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {showBehavioralPrompt} from 'app/client/components/modals';
|
import {showNewsPopup, showTipPopup} from 'app/client/components/modals';
|
||||||
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
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';
|
||||||
@ -7,40 +7,42 @@ import {isNarrowScreen} from 'app/client/ui2018/cssVars';
|
|||||||
import {BehavioralPrompt, BehavioralPromptPrefs} from 'app/common/Prefs';
|
import {BehavioralPrompt, BehavioralPromptPrefs} from 'app/common/Prefs';
|
||||||
import {getGristConfig} from 'app/common/urlUtils';
|
import {getGristConfig} from 'app/common/urlUtils';
|
||||||
import {Computed, Disposable, dom, Observable} from 'grainjs';
|
import {Computed, Disposable, dom, Observable} from 'grainjs';
|
||||||
import {IPopupOptions} from 'popweasel';
|
import {IPopupOptions, PopupControl} from 'popweasel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for showing a tip.
|
* Options for showing a popup.
|
||||||
*/
|
*/
|
||||||
export interface ShowTipOptions {
|
export interface ShowPopupOptions {
|
||||||
/** Defaults to `false`. */
|
/** Defaults to `false`. Only applies to "tip" popups. */
|
||||||
hideArrow?: boolean;
|
hideArrow?: boolean;
|
||||||
popupOptions?: IPopupOptions;
|
popupOptions?: IPopupOptions;
|
||||||
onDispose?(): void;
|
onDispose?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for attaching a tip to a DOM element.
|
* Options for attaching a popup to a DOM element.
|
||||||
*/
|
*/
|
||||||
export interface AttachTipOptions extends ShowTipOptions {
|
export interface AttachPopupOptions extends ShowPopupOptions {
|
||||||
/**
|
/**
|
||||||
* Optional callback that should return true if the tip should be disabled.
|
* Optional callback that should return true if the popup should be disabled.
|
||||||
*
|
*
|
||||||
* If omitted, the tip is enabled.
|
* If omitted, the popup is enabled.
|
||||||
*/
|
*/
|
||||||
isDisabled?(): boolean;
|
isDisabled?(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueuedTip {
|
interface QueuedPopup {
|
||||||
prompt: BehavioralPrompt;
|
prompt: BehavioralPrompt;
|
||||||
refElement: Element;
|
refElement: Element;
|
||||||
options: ShowTipOptions;
|
options: ShowPopupOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages tips that are shown the first time a user performs some action.
|
* Manages popups for product announcements and tips.
|
||||||
*
|
*
|
||||||
* Tips are shown in the order that they are attached.
|
* Popups are shown in the order that they are attached, with at most one popup
|
||||||
|
* visible at any point in time. Popups that aren't visible are queued until all
|
||||||
|
* preceding popups have been dismissed.
|
||||||
*/
|
*/
|
||||||
export class BehavioralPromptsManager extends Disposable {
|
export class BehavioralPromptsManager extends Disposable {
|
||||||
private _isDisabled: boolean = false;
|
private _isDisabled: boolean = false;
|
||||||
@ -48,37 +50,39 @@ export class BehavioralPromptsManager extends Disposable {
|
|||||||
private readonly _prefs = getUserPrefObs(this._appModel.userPrefsObs, 'behavioralPrompts',
|
private readonly _prefs = getUserPrefObs(this._appModel.userPrefsObs, 'behavioralPrompts',
|
||||||
{ defaultValue: { dontShowTips: false, dismissedTips: [] } }) as Observable<BehavioralPromptPrefs>;
|
{ defaultValue: { dontShowTips: false, dismissedTips: [] } }) as Observable<BehavioralPromptPrefs>;
|
||||||
|
|
||||||
private _dismissedTips: Computed<Set<BehavioralPrompt>> = Computed.create(this, use => {
|
private _dismissedPopups: Computed<Set<BehavioralPrompt>> = Computed.create(this, use => {
|
||||||
const {dismissedTips} = use(this._prefs);
|
const {dismissedTips} = use(this._prefs);
|
||||||
return new Set(dismissedTips.filter(BehavioralPrompt.guard));
|
return new Set(dismissedTips.filter(BehavioralPrompt.guard));
|
||||||
});
|
});
|
||||||
|
|
||||||
private _queuedTips: QueuedTip[] = [];
|
private _queuedPopups: QueuedPopup[] = [];
|
||||||
|
|
||||||
|
private _activePopupCtl: PopupControl<IPopupOptions>;
|
||||||
|
|
||||||
constructor(private _appModel: AppModel) {
|
constructor(private _appModel: AppModel) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public showTip(refElement: Element, prompt: BehavioralPrompt, options: ShowTipOptions = {}) {
|
public showPopup(refElement: Element, prompt: BehavioralPrompt, options: ShowPopupOptions = {}) {
|
||||||
this._queueTip(refElement, prompt, options);
|
this._queuePopup(refElement, prompt, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public attachTip(prompt: BehavioralPrompt, options: AttachTipOptions = {}) {
|
public attachPopup(prompt: BehavioralPrompt, options: AttachPopupOptions = {}) {
|
||||||
return (element: Element) => {
|
return (element: Element) => {
|
||||||
if (options.isDisabled?.()) { return; }
|
if (options.isDisabled?.()) { return; }
|
||||||
|
|
||||||
this._queueTip(element, prompt, options);
|
this._queuePopup(element, prompt, options);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasSeenTip(prompt: BehavioralPrompt) {
|
public hasSeenPopup(prompt: BehavioralPrompt) {
|
||||||
return this._dismissedTips.get().has(prompt);
|
return this._dismissedPopups.get().has(prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldShowTip(prompt: BehavioralPrompt): boolean {
|
public shouldShowPopup(prompt: BehavioralPrompt): boolean {
|
||||||
if (this._isDisabled) { return false; }
|
if (this._isDisabled) { return false; }
|
||||||
|
|
||||||
// For non-SaaS flavors of Grist, don't show tips if the Help Center is explicitly
|
// For non-SaaS flavors of Grist, don't show popups if the Help Center is explicitly
|
||||||
// disabled. A separate opt-out feature could be added down the road for more granularity,
|
// disabled. A separate opt-out feature could be added down the road for more granularity,
|
||||||
// but will require communication in advance to avoid disrupting users.
|
// but will require communication in advance to avoid disrupting users.
|
||||||
const {deploymentType, features} = getGristConfig();
|
const {deploymentType, features} = getGristConfig();
|
||||||
@ -91,22 +95,35 @@ export class BehavioralPromptsManager extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
showContext = 'desktop',
|
popupType,
|
||||||
showDeploymentTypes,
|
audience = 'everyone',
|
||||||
|
deviceType = 'desktop',
|
||||||
|
deploymentTypes,
|
||||||
forceShow = false,
|
forceShow = false,
|
||||||
} = GristBehavioralPrompts[prompt];
|
} = GristBehavioralPrompts[prompt];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
showDeploymentTypes !== '*' &&
|
(audience === 'anonymous-users' && this._appModel.currentValidUser) ||
|
||||||
(!deploymentType || !showDeploymentTypes.includes(deploymentType))
|
(audience === 'signed-in-users' && !this._appModel.currentValidUser)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = isNarrowScreen() ? 'mobile' : 'desktop';
|
if (
|
||||||
if (showContext !== '*' && showContext !== context) { return false; }
|
deploymentTypes !== 'all' &&
|
||||||
|
(!deploymentType || !deploymentTypes.includes(deploymentType))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return forceShow || (!this._prefs.get().dontShowTips && !this.hasSeenTip(prompt));
|
const currentDeviceType = isNarrowScreen() ? 'mobile' : 'desktop';
|
||||||
|
if (deviceType !== 'all' && deviceType !== currentDeviceType) { return false; }
|
||||||
|
|
||||||
|
return (
|
||||||
|
forceShow ||
|
||||||
|
(popupType === 'news' && !this.hasSeenPopup(prompt)) ||
|
||||||
|
(!this._prefs.get().dontShowTips && !this.hasSeenPopup(prompt))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enable() {
|
public enable() {
|
||||||
@ -115,6 +132,12 @@ export class BehavioralPromptsManager extends Disposable {
|
|||||||
|
|
||||||
public disable() {
|
public disable() {
|
||||||
this._isDisabled = true;
|
this._isDisabled = true;
|
||||||
|
this._removeQueuedPopups();
|
||||||
|
this._removeActivePopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isDisabled() {
|
||||||
|
return this._isDisabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public reset() {
|
public reset() {
|
||||||
@ -122,58 +145,70 @@ export class BehavioralPromptsManager extends Disposable {
|
|||||||
this.enable();
|
this.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _queueTip(refElement: Element, prompt: BehavioralPrompt, options: ShowTipOptions) {
|
private _queuePopup(refElement: Element, prompt: BehavioralPrompt, options: ShowPopupOptions) {
|
||||||
if (!this.shouldShowTip(prompt)) { return; }
|
if (!this.shouldShowPopup(prompt)) { return; }
|
||||||
|
|
||||||
this._queuedTips.push({prompt, refElement, options});
|
this._queuedPopups.push({prompt, refElement, options});
|
||||||
if (this._queuedTips.length > 1) {
|
if (this._queuedPopups.length > 1) {
|
||||||
// If we're already showing a tip, wait for that one to be dismissed, which will
|
// If we're already showing a popup, wait for that one to be dismissed, which will
|
||||||
// cause the next one in the queue to be shown.
|
// cause the next one in the queue to be shown.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._showTip(refElement, prompt, options);
|
this._showPopup(refElement, prompt, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _showTip(refElement: Element, prompt: BehavioralPrompt, options: ShowTipOptions) {
|
private _showPopup(refElement: Element, prompt: BehavioralPrompt, options: ShowPopupOptions) {
|
||||||
|
const {hideArrow, onDispose, popupOptions} = options;
|
||||||
|
const {popupType, title, content, hideDontShowTips = false, markAsSeen = true} = GristBehavioralPrompts[prompt];
|
||||||
|
let ctl: PopupControl<IPopupOptions>;
|
||||||
|
if (popupType === 'news') {
|
||||||
|
ctl = showNewsPopup(refElement, title(), content(), {
|
||||||
|
popupOptions,
|
||||||
|
});
|
||||||
|
ctl.onDispose(() => { if (markAsSeen) { this._markAsSeen(prompt); } });
|
||||||
|
} else if (popupType === 'tip') {
|
||||||
|
ctl = showTipPopup(refElement, title(), content(), {
|
||||||
|
onClose: (dontShowTips) => {
|
||||||
|
if (dontShowTips) { this._dontShowTips(); }
|
||||||
|
if (markAsSeen) { this._markAsSeen(prompt); }
|
||||||
|
},
|
||||||
|
hideArrow,
|
||||||
|
popupOptions,
|
||||||
|
hideDontShowTips,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(`BehavioralPromptsManager received unknown popup type: ${popupType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._activePopupCtl = ctl;
|
||||||
|
ctl.onDispose(() => {
|
||||||
|
onDispose?.();
|
||||||
|
this._showNextQueuedPopup();
|
||||||
|
});
|
||||||
const close = () => {
|
const close = () => {
|
||||||
if (!ctl.isDisposed()) {
|
if (!ctl.isDisposed()) {
|
||||||
ctl.close();
|
ctl.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const {hideArrow, onDispose, popupOptions} = options;
|
|
||||||
const {title, content, hideDontShowTips = false, markAsSeen = true} = GristBehavioralPrompts[prompt];
|
|
||||||
const ctl = showBehavioralPrompt(refElement, title(), content(), {
|
|
||||||
onClose: (dontShowTips) => {
|
|
||||||
if (dontShowTips) { this._dontShowTips(); }
|
|
||||||
if (markAsSeen) { this._markAsSeen(prompt); }
|
|
||||||
},
|
|
||||||
hideArrow,
|
|
||||||
popupOptions,
|
|
||||||
hideDontShowTips,
|
|
||||||
});
|
|
||||||
|
|
||||||
ctl.onDispose(() => {
|
|
||||||
onDispose?.();
|
|
||||||
this._showNextQueuedTip();
|
|
||||||
});
|
|
||||||
dom.onElem(refElement, 'click', () => close());
|
dom.onElem(refElement, 'click', () => close());
|
||||||
dom.onDisposeElem(refElement, () => close());
|
dom.onDisposeElem(refElement, () => close());
|
||||||
|
|
||||||
logTelemetryEvent('viewedTip', {full: {tipName: prompt}});
|
logTelemetryEvent('viewedTip', {full: {tipName: prompt}});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _showNextQueuedTip() {
|
private _showNextQueuedPopup() {
|
||||||
this._queuedTips.shift();
|
this._queuedPopups.shift();
|
||||||
if (this._queuedTips.length !== 0) {
|
if (this._queuedPopups.length !== 0) {
|
||||||
const [nextTip] = this._queuedTips;
|
const [nextPopup] = this._queuedPopups;
|
||||||
const {refElement, prompt, options} = nextTip;
|
const {refElement, prompt, options} = nextPopup;
|
||||||
this._showTip(refElement, prompt, options);
|
this._showPopup(refElement, prompt, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _markAsSeen(prompt: BehavioralPrompt) {
|
private _markAsSeen(prompt: BehavioralPrompt) {
|
||||||
|
if (this._isDisabled) { return; }
|
||||||
|
|
||||||
const {dismissedTips} = this._prefs.get();
|
const {dismissedTips} = this._prefs.get();
|
||||||
const newDismissedTips = new Set(dismissedTips);
|
const newDismissedTips = new Set(dismissedTips);
|
||||||
newDismissedTips.add(prompt);
|
newDismissedTips.add(prompt);
|
||||||
@ -181,7 +216,21 @@ export class BehavioralPromptsManager extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _dontShowTips() {
|
private _dontShowTips() {
|
||||||
|
if (this._isDisabled) { return; }
|
||||||
|
|
||||||
this._prefs.set({...this._prefs.get(), dontShowTips: true});
|
this._prefs.set({...this._prefs.get(), dontShowTips: true});
|
||||||
this._queuedTips = [];
|
this._queuedPopups = this._queuedPopups.filter(({prompt}) => {
|
||||||
|
return GristBehavioralPrompts[prompt].popupType !== 'tip';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeActivePopup() {
|
||||||
|
if (this._activePopupCtl && !this._activePopupCtl.isDisposed()) {
|
||||||
|
this._activePopupCtl.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeQueuedPopups() {
|
||||||
|
this._queuedPopups = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {buildEditor} from 'app/client/components/Forms/Editor';
|
import {buildEditor} from 'app/client/components/Forms/Editor';
|
||||||
|
import {FieldModel} from 'app/client/components/Forms/Field';
|
||||||
import {buildMenu} from 'app/client/components/Forms/Menu';
|
import {buildMenu} from 'app/client/components/Forms/Menu';
|
||||||
import {BoxModel} from 'app/client/components/Forms/Model';
|
import {BoxModel} from 'app/client/components/Forms/Model';
|
||||||
import * as style from 'app/client/components/Forms/styles';
|
import * as style from 'app/client/components/Forms/styles';
|
||||||
@ -86,6 +87,25 @@ export class ColumnsModel extends BoxModel {
|
|||||||
);
|
);
|
||||||
return buildEditor({ box: this, content });
|
return buildEditor({ box: this, content });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async deleteSelf(): Promise<void> {
|
||||||
|
// Prepare all the fields that are children of this column for removal.
|
||||||
|
const fieldsToRemove = (Array.from(this.filter(b => b instanceof FieldModel)) as FieldModel[]);
|
||||||
|
const fieldIdsToRemove = fieldsToRemove.map(f => f.leaf.get());
|
||||||
|
|
||||||
|
// Remove each child of this column from the layout.
|
||||||
|
this.children.get().forEach(child => { child.removeSelf(); });
|
||||||
|
|
||||||
|
// Remove this column from the layout.
|
||||||
|
this.removeSelf();
|
||||||
|
|
||||||
|
// Finally, remove the fields and save the changes to the layout.
|
||||||
|
await this.parent?.save(async () => {
|
||||||
|
if (fieldIdsToRemove.length > 0) {
|
||||||
|
await this.view.viewSection.removeField(fieldIdsToRemove);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PlaceholderModel extends BoxModel {
|
export class PlaceholderModel extends BoxModel {
|
||||||
|
30
app/client/components/Forms/FormConfig.ts
Normal file
30
app/client/components/Forms/FormConfig.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {fromKoSave} from 'app/client/lib/fromKoSave';
|
||||||
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
import {ViewFieldRec} from 'app/client/models/DocModel';
|
||||||
|
import {KoSaveableObservable} from 'app/client/models/modelUtil';
|
||||||
|
import {cssLabel, cssRow, cssSeparator} from 'app/client/ui/RightPanelStyles';
|
||||||
|
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
||||||
|
import {testId} from 'app/client/ui2018/cssVars';
|
||||||
|
import {Disposable} from 'grainjs';
|
||||||
|
|
||||||
|
const t = makeT('FormConfig');
|
||||||
|
|
||||||
|
export class FieldRulesConfig extends Disposable {
|
||||||
|
constructor(private _field: ViewFieldRec) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildDom() {
|
||||||
|
const requiredField: KoSaveableObservable<boolean> = this._field.widgetOptionsJson.prop('formRequired');
|
||||||
|
|
||||||
|
return [
|
||||||
|
cssSeparator(),
|
||||||
|
cssLabel(t('Field rules')),
|
||||||
|
cssRow(labeledSquareCheckbox(
|
||||||
|
fromKoSave(requiredField),
|
||||||
|
t('Required field'),
|
||||||
|
testId('field-required'),
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -644,28 +644,14 @@ export class FormView extends Disposable {
|
|||||||
dom.on('click', async (_event, element) => {
|
dom.on('click', async (_event, element) => {
|
||||||
try {
|
try {
|
||||||
this._copyingLink.set(true);
|
this._copyingLink.set(true);
|
||||||
const share = this._pageShare.get();
|
const data = typeof ClipboardItem !== 'function' ? await this._getFormLink() : new ClipboardItem({
|
||||||
if (!share) {
|
"text/plain": this._getFormLink().then(text => new Blob([text], {type: 'text/plain'})),
|
||||||
throw new Error('Unable to copy link: form is not published');
|
|
||||||
}
|
|
||||||
|
|
||||||
const remoteShare = await this.gristDoc.docComm.getShare(share.linkId());
|
|
||||||
if (!remoteShare) {
|
|
||||||
throw new Error('Unable to copy link: form is not published');
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = urlState().makeUrl({
|
|
||||||
doc: undefined,
|
|
||||||
form: {
|
|
||||||
shareKey: remoteShare.key,
|
|
||||||
vsId: this.viewSection.id(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
await copyToClipboard(url);
|
await copyToClipboard(data);
|
||||||
showTransientTooltip(element, 'Link copied to clipboard', {key: 'copy-form-link'});
|
showTransientTooltip(element, 'Link copied to clipboard', {key: 'copy-form-link'});
|
||||||
} catch(ex) {
|
} catch (ex) {
|
||||||
if (ex.code === 'AUTH_NO_OWNER') {
|
if (ex.code === 'AUTH_NO_OWNER') {
|
||||||
throw new Error('Publishing form is only available to owners');
|
throw new Error('Sharing a form is only available to owners');
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this._copyingLink.set(false);
|
this._copyingLink.set(false);
|
||||||
@ -693,6 +679,26 @@ export class FormView extends Disposable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _getFormLink() {
|
||||||
|
const share = this._pageShare.get();
|
||||||
|
if (!share) {
|
||||||
|
throw new Error('Unable to get form link: form is not published');
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteShare = await this.gristDoc.docComm.getShare(share.linkId());
|
||||||
|
if (!remoteShare) {
|
||||||
|
throw new Error('Unable to get form link: form is not published');
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlState().makeUrl({
|
||||||
|
doc: undefined,
|
||||||
|
form: {
|
||||||
|
shareKey: remoteShare.key,
|
||||||
|
vsId: this.viewSection.id(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private _buildSwitcherMessage() {
|
private _buildSwitcherMessage() {
|
||||||
return dom.maybe(use => use(this._published) && use(this._showPublishedMessage), () => {
|
return dom.maybe(use => use(this._published) && use(this._showPublishedMessage), () => {
|
||||||
return style.cssSwitcherMessage(
|
return style.cssSwitcherMessage(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import * as style from './styles';
|
import * as style from './styles';
|
||||||
import {buildEditor} from 'app/client/components/Forms/Editor';
|
import {buildEditor} from 'app/client/components/Forms/Editor';
|
||||||
|
import {FieldModel} from 'app/client/components/Forms/Field';
|
||||||
import {buildMenu} from 'app/client/components/Forms/Menu';
|
import {buildMenu} from 'app/client/components/Forms/Menu';
|
||||||
import {BoxModel} from 'app/client/components/Forms/Model';
|
import {BoxModel} from 'app/client/components/Forms/Model';
|
||||||
import {makeTestId} from 'app/client/lib/domUtils';
|
import {makeTestId} from 'app/client/lib/domUtils';
|
||||||
@ -72,6 +73,25 @@ export class SectionModel extends BoxModel {
|
|||||||
|
|
||||||
return place(dropped);
|
return place(dropped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async deleteSelf(): Promise<void> {
|
||||||
|
// Prepare all the fields that are children of this section for removal.
|
||||||
|
const fieldsToRemove = Array.from(this.filter(b => b instanceof FieldModel)) as FieldModel[];
|
||||||
|
const fieldIdsToRemove = fieldsToRemove.map(f => f.leaf.get());
|
||||||
|
|
||||||
|
// Remove each child of this section from the layout.
|
||||||
|
this.children.get().forEach(child => { child.removeSelf(); });
|
||||||
|
|
||||||
|
// Remove this section from the layout.
|
||||||
|
this.removeSelf();
|
||||||
|
|
||||||
|
// Finally, remove the fields and save the changes to the layout.
|
||||||
|
await this.parent?.save(async () => {
|
||||||
|
if (fieldIdsToRemove.length > 0) {
|
||||||
|
await this.view.viewSection.removeField(fieldIdsToRemove);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssSectionItems = styled('div.hover_border', `
|
const cssSectionItems = styled('div.hover_border', `
|
||||||
|
@ -150,7 +150,7 @@ export class UnmappedFieldsConfig extends Disposable {
|
|||||||
allCommands.showColumns.run([column.colId.peek()]);
|
allCommands.showColumns.run([column.colId.peek()]);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
squareCheckbox(props.selected),
|
cssSquareCheckbox(props.selected),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -171,7 +171,7 @@ export class UnmappedFieldsConfig extends Disposable {
|
|||||||
allCommands.hideFields.run([column.colId.peek()]);
|
allCommands.hideFields.run([column.colId.peek()]);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
squareCheckbox(props.selected),
|
cssSquareCheckbox(props.selected),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -272,3 +272,7 @@ const cssHeader = styled(cssRow, `
|
|||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssSquareCheckbox = styled(squareCheckbox, `
|
||||||
|
flex-shrink: 0;
|
||||||
|
`);
|
||||||
|
@ -147,7 +147,7 @@ export const cssRenderedLabel = styled('div', `
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
min-height: 16px;
|
min-height: 16px;
|
||||||
|
|
||||||
color: ${colors.darkText};
|
color: ${theme.mediumText};
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -213,6 +213,7 @@ export const cssDesc = styled('div', `
|
|||||||
export const cssInput = styled('input', `
|
export const cssInput = styled('input', `
|
||||||
background-color: ${theme.inputDisabledBg};
|
background-color: ${theme.inputDisabledBg};
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
height: 27px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: 1px solid ${theme.inputBorder};
|
border: 1px solid ${theme.inputBorder};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
@ -232,6 +233,7 @@ export const cssSelect = styled('select', `
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: ${theme.inputDisabledBg};
|
background-color: ${theme.inputDisabledBg};
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
height: 27px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: 1px solid ${theme.inputBorder};
|
border: 1px solid ${theme.inputBorder};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -45,7 +45,7 @@ import {DocHistory} from 'app/client/ui/DocHistory';
|
|||||||
import {startDocTour} from "app/client/ui/DocTour";
|
import {startDocTour} from "app/client/ui/DocTour";
|
||||||
import {DocTutorial} from 'app/client/ui/DocTutorial';
|
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, isTourActiveObs} 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, NoLink, 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';
|
||||||
@ -299,14 +299,12 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
// Subscribe to URL state, and navigate to anchor or open a popup if necessary.
|
// Subscribe to URL state, and navigate to anchor or open a popup if necessary.
|
||||||
this.autoDispose(subscribe(urlState().state, async (use, state) => {
|
this.autoDispose(subscribe(urlState().state, async (use, state) => {
|
||||||
if (!state.hash) {
|
if (!state.hash) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (state.hash.popup || state.hash.recordCard) {
|
if (state.hash.popup || state.hash.recordCard) {
|
||||||
await this._openPopup(state.hash);
|
await this._openPopup(state.hash);
|
||||||
@ -343,7 +341,7 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.behavioralPromptsManager.showTip(cursor, 'rickRow', {
|
this.behavioralPromptsManager.showPopup(cursor, 'rickRow', {
|
||||||
onDispose: () => this._playRickRollVideo(),
|
onDispose: () => this._playRickRollVideo(),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -356,9 +354,25 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (this.docModel.isTutorial()) {
|
this.autoDispose(subscribe(
|
||||||
this.behavioralPromptsManager.disable();
|
urlState().state,
|
||||||
}
|
isTourActiveObs(),
|
||||||
|
fromKo(this.docModel.isTutorial),
|
||||||
|
(_use, state, hasActiveTour, isTutorial) => {
|
||||||
|
// Tours and tutorials can interfere with in-product tips and announcements.
|
||||||
|
const hasPendingDocTour = state.docTour || this._shouldAutoStartDocTour();
|
||||||
|
const hasPendingWelcomeTour = state.welcomeTour || this._shouldAutoStartWelcomeTour();
|
||||||
|
const isPopupManagerDisabled = this.behavioralPromptsManager.isDisabled();
|
||||||
|
if (
|
||||||
|
(hasPendingDocTour || hasPendingWelcomeTour || hasActiveTour || isTutorial) &&
|
||||||
|
!isPopupManagerDisabled
|
||||||
|
) {
|
||||||
|
this.behavioralPromptsManager.disable();
|
||||||
|
} else if (isPopupManagerDisabled) {
|
||||||
|
this.behavioralPromptsManager.enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
let isStartingTourOrTutorial = false;
|
let isStartingTourOrTutorial = false;
|
||||||
this.autoDispose(subscribe(urlState().state, async (_use, state) => {
|
this.autoDispose(subscribe(urlState().state, async (_use, state) => {
|
||||||
@ -1611,7 +1625,7 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
// Don't show the tip if a non-card widget was selected.
|
// Don't show the tip if a non-card widget was selected.
|
||||||
!['single', 'detail'].includes(selectedWidgetType) ||
|
!['single', 'detail'].includes(selectedWidgetType) ||
|
||||||
// Or if we shouldn't see the tip.
|
// Or if we shouldn't see the tip.
|
||||||
!this.behavioralPromptsManager.shouldShowTip('editCardLayout')
|
!this.behavioralPromptsManager.shouldShowPopup('editCardLayout')
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1627,7 +1641,7 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
throw new Error('GristDoc failed to find edit card layout button');
|
throw new Error('GristDoc failed to find edit card layout button');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.behavioralPromptsManager.showTip(editLayoutButton, 'editCardLayout', {
|
this.behavioralPromptsManager.showPopup(editLayoutButton, 'editCardLayout', {
|
||||||
popupOptions: {
|
popupOptions: {
|
||||||
placement: 'left-start',
|
placement: 'left-start',
|
||||||
}
|
}
|
||||||
@ -1637,7 +1651,7 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
private async _handleNewAttachedCustomWidget(widget: IAttachedCustomWidget) {
|
private async _handleNewAttachedCustomWidget(widget: IAttachedCustomWidget) {
|
||||||
switch (widget) {
|
switch (widget) {
|
||||||
case 'custom.calendar': {
|
case 'custom.calendar': {
|
||||||
if (this.behavioralPromptsManager.shouldShowTip('calendarConfig')) {
|
if (this.behavioralPromptsManager.shouldShowPopup('calendarConfig')) {
|
||||||
// Open the right panel to the calendar subtab.
|
// Open the right panel to the calendar subtab.
|
||||||
commands.allCommands.viewTabOpen.run();
|
commands.allCommands.viewTabOpen.run();
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ export class RawDataPage extends Disposable {
|
|||||||
public buildDom() {
|
public buildDom() {
|
||||||
return cssContainer(
|
return cssContainer(
|
||||||
cssPage(
|
cssPage(
|
||||||
dom('div', this._gristDoc.behavioralPromptsManager.attachTip('rawDataPage', {hideArrow: true})),
|
dom('div', this._gristDoc.behavioralPromptsManager.attachPopup('rawDataPage', {hideArrow: true})),
|
||||||
dom('div',
|
dom('div',
|
||||||
dom.create(DataTables, this._gristDoc),
|
dom.create(DataTables, this._gristDoc),
|
||||||
dom.create(DocumentUsage, this._gristDoc.docPageModel)
|
dom.create(DocumentUsage, this._gristDoc.docPageModel)
|
||||||
|
@ -18,6 +18,7 @@ import {NewAbstractWidget} from 'app/client/widgets/NewAbstractWidget';
|
|||||||
import {UserAction} from 'app/common/DocActions';
|
import {UserAction} from 'app/common/DocActions';
|
||||||
import {Computed, dom, fromKo, Observable} from 'grainjs';
|
import {Computed, dom, fromKo, Observable} from 'grainjs';
|
||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
|
import {WidgetType} from 'app/common/widgetTypes';
|
||||||
|
|
||||||
const t = makeT('TypeTransform');
|
const t = makeT('TypeTransform');
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ const t = makeT('TypeTransform');
|
|||||||
export class TypeTransform extends ColumnTransform {
|
export class TypeTransform extends ColumnTransform {
|
||||||
private _reviseTypeChange = Observable.create(this, false);
|
private _reviseTypeChange = Observable.create(this, false);
|
||||||
private _transformWidget: Computed<NewAbstractWidget|null>;
|
private _transformWidget: Computed<NewAbstractWidget|null>;
|
||||||
|
private _isFormWidget: Computed<boolean>;
|
||||||
private _convertColumn: ColumnRec; // Set in prepare()
|
private _convertColumn: ColumnRec; // Set in prepare()
|
||||||
|
|
||||||
constructor(gristDoc: GristDoc, fieldBuilder: FieldBuilder) {
|
constructor(gristDoc: GristDoc, fieldBuilder: FieldBuilder) {
|
||||||
@ -41,6 +43,8 @@ export class TypeTransform extends ColumnTransform {
|
|||||||
this._transformWidget = Computed.create(this, fromKo(fieldBuilder.widgetImpl), (use, widget) => {
|
this._transformWidget = Computed.create(this, fromKo(fieldBuilder.widgetImpl), (use, widget) => {
|
||||||
return use(this.origColumn.isTransforming) ? widget : null;
|
return use(this.origColumn.isTransforming) ? widget : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._isFormWidget = Computed.create(this, use => use(use(this.field.viewSection).parentKey) === WidgetType.Form);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,7 +56,16 @@ export class TypeTransform extends ColumnTransform {
|
|||||||
this._reviseTypeChange.set(false);
|
this._reviseTypeChange.set(false);
|
||||||
return dom('div',
|
return dom('div',
|
||||||
testId('type-transform-top'),
|
testId('type-transform-top'),
|
||||||
dom.maybe(this._transformWidget, transformWidget => transformWidget.buildTransformConfigDom()),
|
dom.domComputed(use => {
|
||||||
|
const transformWidget = use(this._transformWidget);
|
||||||
|
if (!transformWidget) { return null; }
|
||||||
|
|
||||||
|
if (use(this._isFormWidget)) {
|
||||||
|
return transformWidget.buildFormTransformConfigDom();
|
||||||
|
} else {
|
||||||
|
return transformWidget.buildTransformConfigDom();
|
||||||
|
}
|
||||||
|
}),
|
||||||
dom.maybe(this._reviseTypeChange, () =>
|
dom.maybe(this._reviseTypeChange, () =>
|
||||||
dom('div.transform_editor', this.buildEditorDom(),
|
dom('div.transform_editor', this.buildEditorDom(),
|
||||||
testId("type-transform-formula")
|
testId("type-transform-formula")
|
||||||
|
@ -139,7 +139,7 @@ export function reportUndo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShowBehavioralPromptOptions {
|
export interface ShowTipPopupOptions {
|
||||||
onClose: (dontShowTips: boolean) => void;
|
onClose: (dontShowTips: boolean) => void;
|
||||||
/** Defaults to false. */
|
/** Defaults to false. */
|
||||||
hideArrow?: boolean;
|
hideArrow?: boolean;
|
||||||
@ -148,11 +148,11 @@ export interface ShowBehavioralPromptOptions {
|
|||||||
popupOptions?: IPopupOptions;
|
popupOptions?: IPopupOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showBehavioralPrompt(
|
export function showTipPopup(
|
||||||
refElement: Element,
|
refElement: Element,
|
||||||
title: string,
|
title: string,
|
||||||
content: DomContents,
|
content: DomContents,
|
||||||
options: ShowBehavioralPromptOptions
|
options: ShowTipPopupOptions
|
||||||
) {
|
) {
|
||||||
const {onClose, hideArrow = false, hideDontShowTips = false, popupOptions} = options;
|
const {onClose, hideArrow = false, hideDontShowTips = false, popupOptions} = options;
|
||||||
const arrow = hideArrow ? null : buildArrow();
|
const arrow = hideArrow ? null : buildArrow();
|
||||||
@ -196,22 +196,7 @@ export function showBehavioralPrompt(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
merge(popupOptions, {
|
merge({}, defaultPopupOptions, popupOptions),
|
||||||
modifiers: {
|
|
||||||
...(arrow ? {arrow: {element: arrow}}: {}),
|
|
||||||
offset: {
|
|
||||||
offset: '0,12',
|
|
||||||
},
|
|
||||||
preventOverflow: {
|
|
||||||
boundariesElement: 'window',
|
|
||||||
padding: 32,
|
|
||||||
},
|
|
||||||
computeStyle: {
|
|
||||||
// GPU acceleration makes text look blurry.
|
|
||||||
gpuAcceleration: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
dom.onDisposeElem(refElement, () => {
|
dom.onDisposeElem(refElement, () => {
|
||||||
if (!tooltip.isDisposed()) {
|
if (!tooltip.isDisposed()) {
|
||||||
@ -221,6 +206,64 @@ export function showBehavioralPrompt(
|
|||||||
return tooltip;
|
return tooltip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ShowNewsPopupOptions {
|
||||||
|
popupOptions?: IPopupOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showNewsPopup(
|
||||||
|
refElement: Element,
|
||||||
|
title: string,
|
||||||
|
content: DomContents,
|
||||||
|
options: ShowNewsPopupOptions = {}
|
||||||
|
) {
|
||||||
|
const {popupOptions} = options;
|
||||||
|
const popup = modalTooltip(refElement,
|
||||||
|
(ctl) => [
|
||||||
|
cssNewsPopupModal.cls(''),
|
||||||
|
cssNewsPopupContainer(
|
||||||
|
testId('behavioral-prompt'),
|
||||||
|
elem => { FocusLayer.create(ctl, {defaultFocusElem: elem, pauseMousetrap: true}); },
|
||||||
|
dom.onKeyDown({
|
||||||
|
Escape: () => { ctl.close(); },
|
||||||
|
Enter: () => { ctl.close(); },
|
||||||
|
}),
|
||||||
|
cssNewsPopupCloseButton(
|
||||||
|
icon('CrossBig'),
|
||||||
|
dom.on('click', () => ctl.close()),
|
||||||
|
testId('behavioral-prompt-dismiss'),
|
||||||
|
),
|
||||||
|
cssNewsPopupBody(
|
||||||
|
cssNewsPopupTitle(title, testId('behavioral-prompt-title')),
|
||||||
|
content,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
merge({}, defaultPopupOptions, popupOptions),
|
||||||
|
);
|
||||||
|
dom.onDisposeElem(refElement, () => {
|
||||||
|
if (!popup.isDisposed()) {
|
||||||
|
popup.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return popup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPopupOptions = {
|
||||||
|
modifiers: {
|
||||||
|
offset: {
|
||||||
|
offset: '0,12',
|
||||||
|
},
|
||||||
|
preventOverflow: {
|
||||||
|
boundariesElement: 'window',
|
||||||
|
padding: 32,
|
||||||
|
},
|
||||||
|
computeStyle: {
|
||||||
|
// GPU acceleration makes text look blurry.
|
||||||
|
gpuAcceleration: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function buildArrow() {
|
function buildArrow() {
|
||||||
return cssArrowContainer(
|
return cssArrowContainer(
|
||||||
svg('svg',
|
svg('svg',
|
||||||
@ -365,10 +408,18 @@ const cssBehavioralPromptModal = styled('div', `
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssNewsPopupModal = cssBehavioralPromptModal;
|
||||||
|
|
||||||
const cssBehavioralPromptContainer = styled(cssTheme, `
|
const cssBehavioralPromptContainer = styled(cssTheme, `
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssNewsPopupContainer = styled('div', `
|
||||||
|
background: linear-gradient(to right, #29a3a3, #16a772);
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
`);
|
||||||
|
|
||||||
const cssBehavioralPromptHeader = styled('div', `
|
const cssBehavioralPromptHeader = styled('div', `
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -383,6 +434,12 @@ const cssBehavioralPromptBody = styled('div', `
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssNewsPopupBody = styled('div', `
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 23px;
|
||||||
|
padding: 16px;
|
||||||
|
`);
|
||||||
|
|
||||||
const cssHeaderIconAndText = styled('div', `
|
const cssHeaderIconAndText = styled('div', `
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -405,6 +462,27 @@ const cssBehavioralPromptTitle = styled('div', `
|
|||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssNewsPopupTitle = styled('div', `
|
||||||
|
font-size: ${vars.xxxlargeFontSize};
|
||||||
|
font-weight: ${vars.headerControlTextWeight};
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
line-height: 32px;
|
||||||
|
`);
|
||||||
|
|
||||||
|
const cssNewsPopupCloseButton = styled('div', `
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
--icon-color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${theme.hover};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
const cssSkipTipsCheckbox = styled(labeledSquareCheckbox, `
|
const cssSkipTipsCheckbox = styled(labeledSquareCheckbox, `
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
`);
|
`);
|
||||||
|
@ -221,6 +221,11 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
|
|||||||
// "Add New" menu should have the same width as the "Add New" button that opens it.
|
// "Add New" menu should have the same width as the "Add New" button that opens it.
|
||||||
stretchToSelector: `.${cssAddNewButton.className}`
|
stretchToSelector: `.${cssAddNewButton.className}`
|
||||||
}),
|
}),
|
||||||
|
activeDoc.behavioralPromptsManager.attachPopup('formsAreHere', {
|
||||||
|
popupOptions: {
|
||||||
|
placement: 'right',
|
||||||
|
},
|
||||||
|
}),
|
||||||
testId('dp-add-new'),
|
testId('dp-add-new'),
|
||||||
dom.cls('tour-add-new'),
|
dom.cls('tour-add-new'),
|
||||||
),
|
),
|
||||||
|
@ -158,7 +158,7 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
|||||||
wss.every((ws) => ws.isSupportWorkspace || ws.docs.length === 0)));
|
wss.every((ws) => ws.isSupportWorkspace || ws.docs.length === 0)));
|
||||||
|
|
||||||
public readonly shouldShowAddNewTip = Observable.create(this,
|
public readonly shouldShowAddNewTip = Observable.create(this,
|
||||||
!this._app.behavioralPromptsManager.hasSeenTip('addNew'));
|
!this._app.behavioralPromptsManager.hasSeenPopup('addNew'));
|
||||||
|
|
||||||
private _userOrgPrefs = Observable.create<UserOrgPrefs|undefined>(this, this._app.currentOrg?.userOrgPrefs);
|
private _userOrgPrefs = Observable.create<UserOrgPrefs|undefined>(this, this._app.currentOrg?.userOrgPrefs);
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ function showAddNewTip(home: HomeModel): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
home.app.behavioralPromptsManager.showTip(addNewButton, 'addNew', {
|
home.app.behavioralPromptsManager.showPopup(addNewButton, 'addNew', {
|
||||||
popupOptions: {
|
popupOptions: {
|
||||||
placement: 'right-start',
|
placement: 'right-start',
|
||||||
},
|
},
|
||||||
|
@ -349,7 +349,7 @@ export function columnFilterMenu(owner: IDisposableOwner, opts: IFilterMenuOptio
|
|||||||
icon('PinTilted'),
|
icon('PinTilted'),
|
||||||
cssPinButton.cls('-pinned', model.filterInfo.isPinned),
|
cssPinButton.cls('-pinned', model.filterInfo.isPinned),
|
||||||
dom.on('click', () => filterInfo.pinned(!filterInfo.pinned())),
|
dom.on('click', () => filterInfo.pinned(!filterInfo.pinned())),
|
||||||
gristDoc.behavioralPromptsManager.attachTip('filterButtons', {
|
gristDoc.behavioralPromptsManager.attachPopup('filterButtons', {
|
||||||
popupOptions: {
|
popupOptions: {
|
||||||
attach: null,
|
attach: null,
|
||||||
placement: 'right',
|
placement: 'right',
|
||||||
|
@ -373,7 +373,7 @@ class CustomSectionConfigurationConfig extends Disposable{
|
|||||||
switch (widgetUrl) {
|
switch (widgetUrl) {
|
||||||
// TODO: come up with a way to attach tips without hardcoding widget URLs.
|
// TODO: come up with a way to attach tips without hardcoding widget URLs.
|
||||||
case 'https://gristlabs.github.io/grist-widget/calendar/index.html': {
|
case 'https://gristlabs.github.io/grist-widget/calendar/index.html': {
|
||||||
return this._gristDoc.behavioralPromptsManager.attachTip('calendarConfig', {
|
return this._gristDoc.behavioralPromptsManager.attachPopup('calendarConfig', {
|
||||||
popupOptions: {placement: 'left-start'},
|
popupOptions: {placement: 'left-start'},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -600,7 +600,7 @@ export class CustomSectionConfig extends Disposable {
|
|||||||
dom.attr('placeholder', t("Enter Custom URL")),
|
dom.attr('placeholder', t("Enter Custom URL")),
|
||||||
testId('url')
|
testId('url')
|
||||||
),
|
),
|
||||||
this._gristDoc.behavioralPromptsManager.attachTip('customURL', {
|
this._gristDoc.behavioralPromptsManager.attachPopup('customURL', {
|
||||||
popupOptions: {
|
popupOptions: {
|
||||||
placement: 'left-start',
|
placement: 'left-start',
|
||||||
},
|
},
|
||||||
|
@ -24,7 +24,7 @@ export function filterBar(
|
|||||||
dom.forEach(viewSection.activeFilters, (filterInfo) => makeFilterField(filterInfo, popupControls)),
|
dom.forEach(viewSection.activeFilters, (filterInfo) => makeFilterField(filterInfo, popupControls)),
|
||||||
dom.maybe(viewSection.showNestedFilteringPopup, () => {
|
dom.maybe(viewSection.showNestedFilteringPopup, () => {
|
||||||
return dom('div',
|
return dom('div',
|
||||||
gristDoc.behavioralPromptsManager.attachTip('nestedFiltering', {
|
gristDoc.behavioralPromptsManager.attachPopup('nestedFiltering', {
|
||||||
onDispose: () => viewSection.showNestedFilteringPopup.set(false),
|
onDispose: () => viewSection.showNestedFilteringPopup.set(false),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -108,7 +108,7 @@ function buildAddNewColumMenuSection(gridView: GridView, index?: number): DomEle
|
|||||||
},
|
},
|
||||||
menuIcon(colType.icon as IconName),
|
menuIcon(colType.icon as IconName),
|
||||||
colType.displayName === 'Reference'?
|
colType.displayName === 'Reference'?
|
||||||
gridView.gristDoc.behavioralPromptsManager.attachTip('referenceColumns', {
|
gridView.gristDoc.behavioralPromptsManager.attachPopup('referenceColumns', {
|
||||||
popupOptions: {
|
popupOptions: {
|
||||||
attach: `.${menuCssClass}`,
|
attach: `.${menuCssClass}`,
|
||||||
placement: 'left-start',
|
placement: 'left-start',
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as commands from 'app/client/components/commands';
|
import * as commands from 'app/client/components/commands';
|
||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
import {ShortcutKey, ShortcutKeyContent} from 'app/client/ui/ShortcutKey';
|
import {ShortcutKey, ShortcutKeyContent} from 'app/client/ui/ShortcutKey';
|
||||||
|
import {basicButtonLink} from 'app/client/ui2018/buttons';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {cssLink} from 'app/client/ui2018/links';
|
import {cssLink} from 'app/client/ui2018/links';
|
||||||
import {commonUrls, GristDeploymentType} from 'app/common/gristUrls';
|
import {commonUrls, GristDeploymentType} from 'app/common/gristUrls';
|
||||||
@ -28,6 +29,17 @@ const cssIcon = styled(icon, `
|
|||||||
width: 18px;
|
width: 18px;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const cssNewsPopupLearnMoreButton = styled(basicButtonLink, `
|
||||||
|
color: white;
|
||||||
|
border: 1px solid white;
|
||||||
|
padding: 3px;
|
||||||
|
|
||||||
|
&:hover, &:focus, &:visited {
|
||||||
|
color: white;
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
export type Tooltip =
|
export type Tooltip =
|
||||||
| 'dataSize'
|
| 'dataSize'
|
||||||
| 'setTriggerFormula'
|
| 'setTriggerFormula'
|
||||||
@ -126,11 +138,14 @@ see or edit which parts of your document.')
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface BehavioralPromptContent {
|
export interface BehavioralPromptContent {
|
||||||
|
popupType: 'tip' | 'news';
|
||||||
title: () => string;
|
title: () => string;
|
||||||
content: (...domArgs: DomElementArg[]) => DomContents;
|
content: (...domArgs: DomElementArg[]) => DomContents;
|
||||||
showDeploymentTypes: GristDeploymentType[] | '*';
|
deploymentTypes: GristDeploymentType[] | 'all';
|
||||||
|
/** Defaults to `everyone`. */
|
||||||
|
audience?: 'signed-in-users' | 'anonymous-users' | 'everyone';
|
||||||
/** Defaults to `desktop`. */
|
/** Defaults to `desktop`. */
|
||||||
showContext?: 'mobile' | 'desktop' | '*';
|
deviceType?: 'mobile' | 'desktop' | 'all';
|
||||||
/** Defaults to `false`. */
|
/** Defaults to `false`. */
|
||||||
hideDontShowTips?: boolean;
|
hideDontShowTips?: boolean;
|
||||||
/** Defaults to `false`. */
|
/** Defaults to `false`. */
|
||||||
@ -141,6 +156,7 @@ export interface BehavioralPromptContent {
|
|||||||
|
|
||||||
export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptContent> = {
|
export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptContent> = {
|
||||||
referenceColumns: {
|
referenceColumns: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Reference Columns'),
|
title: () => t('Reference Columns'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('Reference columns are the key to {{relational}} data in Grist.', {
|
dom('div', t('Reference columns are the key to {{relational}} data in Grist.', {
|
||||||
@ -152,9 +168,10 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
|
|||||||
),
|
),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
||||||
},
|
},
|
||||||
referenceColumnsConfig: {
|
referenceColumnsConfig: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Reference Columns'),
|
title: () => t('Reference Columns'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('Select the table to link to.')),
|
dom('div', t('Select the table to link to.')),
|
||||||
@ -167,9 +184,10 @@ record in that table, but you may select which column from that record to show.'
|
|||||||
),
|
),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
||||||
},
|
},
|
||||||
rawDataPage: {
|
rawDataPage: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Raw Data page'),
|
title: () => t('Raw Data page'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('The Raw Data page lists all data tables in your document, \
|
dom('div', t('The Raw Data page lists all data tables in your document, \
|
||||||
@ -177,9 +195,10 @@ including summary tables and tables not included in page layouts.')),
|
|||||||
dom('div', cssLink({href: commonUrls.helpRawData, target: '_blank'}, t('Learn more.'))),
|
dom('div', cssLink({href: commonUrls.helpRawData, target: '_blank'}, t('Learn more.'))),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
||||||
},
|
},
|
||||||
accessRules: {
|
accessRules: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Access Rules'),
|
title: () => t('Access Rules'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('Access rules give you the power to create nuanced rules \
|
dom('div', t('Access rules give you the power to create nuanced rules \
|
||||||
@ -187,9 +206,10 @@ to determine who can see or edit which parts of your document.')),
|
|||||||
dom('div', cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.'))),
|
dom('div', cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.'))),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
||||||
},
|
},
|
||||||
filterButtons: {
|
filterButtons: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Pinning Filters'),
|
title: () => t('Pinning Filters'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('Pinned filters are displayed as buttons above the widget.')),
|
dom('div', t('Pinned filters are displayed as buttons above the widget.')),
|
||||||
@ -197,27 +217,30 @@ to determine who can see or edit which parts of your document.')),
|
|||||||
dom('div', cssLink({href: commonUrls.helpFilterButtons, target: '_blank'}, t('Learn more.'))),
|
dom('div', cssLink({href: commonUrls.helpFilterButtons, target: '_blank'}, t('Learn more.'))),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
||||||
},
|
},
|
||||||
nestedFiltering: {
|
nestedFiltering: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Nested Filtering'),
|
title: () => t('Nested Filtering'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('You can filter by more than one column.')),
|
dom('div', t('You can filter by more than one column.')),
|
||||||
dom('div', t('Only those rows will appear which match all of the filters.')),
|
dom('div', t('Only those rows will appear which match all of the filters.')),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
||||||
},
|
},
|
||||||
pageWidgetPicker: {
|
pageWidgetPicker: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Selecting Data'),
|
title: () => t('Selecting Data'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('Select the table containing the data to show.')),
|
dom('div', t('Select the table containing the data to show.')),
|
||||||
dom('div', t('Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.')),
|
dom('div', t('Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.')),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
||||||
},
|
},
|
||||||
pageWidgetPickerSelectBy: {
|
pageWidgetPickerSelectBy: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Linking Widgets'),
|
title: () => t('Linking Widgets'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('Link your new widget to an existing widget on this page.')),
|
dom('div', t('Link your new widget to an existing widget on this page.')),
|
||||||
@ -225,9 +248,10 @@ to determine who can see or edit which parts of your document.')),
|
|||||||
dom('div', cssLink({href: commonUrls.helpLinkingWidgets, target: '_blank'}, t('Learn more.'))),
|
dom('div', cssLink({href: commonUrls.helpLinkingWidgets, target: '_blank'}, t('Learn more.'))),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
||||||
},
|
},
|
||||||
editCardLayout: {
|
editCardLayout: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Editing Card Layout'),
|
title: () => t('Editing Card Layout'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('Rearrange the fields in your card by dragging and resizing cells.')),
|
dom('div', t('Rearrange the fields in your card by dragging and resizing cells.')),
|
||||||
@ -236,17 +260,19 @@ to determine who can see or edit which parts of your document.')),
|
|||||||
})),
|
})),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
||||||
},
|
},
|
||||||
addNew: {
|
addNew: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Add New'),
|
title: () => t('Add New'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('Click the Add New button to create new documents or workspaces, or import data.')),
|
dom('div', t('Click the Add New button to create new documents or workspaces, or import data.')),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
||||||
},
|
},
|
||||||
rickRow: {
|
rickRow: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Anchor Links'),
|
title: () => t('Anchor Links'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div',
|
dom('div',
|
||||||
@ -258,13 +284,14 @@ to determine who can see or edit which parts of your document.')),
|
|||||||
),
|
),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: '*',
|
deploymentTypes: 'all',
|
||||||
showContext: '*',
|
deviceType: 'all',
|
||||||
hideDontShowTips: true,
|
hideDontShowTips: true,
|
||||||
forceShow: true,
|
forceShow: true,
|
||||||
markAsSeen: false,
|
markAsSeen: false,
|
||||||
},
|
},
|
||||||
customURL: {
|
customURL: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Custom Widgets'),
|
title: () => t('Custom Widgets'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div',
|
dom('div',
|
||||||
@ -275,9 +302,10 @@ to determine who can see or edit which parts of your document.')),
|
|||||||
dom('div', cssLink({href: commonUrls.helpCustomWidgets, target: '_blank'}, t('Learn more.'))),
|
dom('div', cssLink({href: commonUrls.helpCustomWidgets, target: '_blank'}, t('Learn more.'))),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
||||||
},
|
},
|
||||||
calendarConfig: {
|
calendarConfig: {
|
||||||
|
popupType: 'tip',
|
||||||
title: () => t('Calendar'),
|
title: () => t('Calendar'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t("To configure your calendar, select columns for start/end dates and event titles. \
|
dom('div', t("To configure your calendar, select columns for start/end dates and event titles. \
|
||||||
@ -287,6 +315,21 @@ data.")),
|
|||||||
dom('div', cssLink({href: commonUrls.helpCalendarWidget, target: '_blank'}, t('Learn more.'))),
|
dom('div', cssLink({href: commonUrls.helpCalendarWidget, target: '_blank'}, t('Learn more.'))),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
|
||||||
|
},
|
||||||
|
formsAreHere: {
|
||||||
|
popupType: 'news',
|
||||||
|
audience: 'signed-in-users',
|
||||||
|
title: () => t('Forms are here!'),
|
||||||
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
|
dom('div', t('Build simple forms right in Grist and share in a click with our new widget. {{learnMoreButton}}', {
|
||||||
|
learnMoreButton: cssNewsPopupLearnMoreButton(t('Learn more'), {
|
||||||
|
href: commonUrls.forms,
|
||||||
|
target: '_blank',
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
...args,
|
||||||
|
),
|
||||||
|
deploymentTypes: ['saas', 'core', 'enterprise'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
* the caller. Pass an `onFinishCB` to handle when a user dimiss the popups.
|
* the caller. Pass an `onFinishCB` to handle when a user dimiss the popups.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Disposable, dom, DomElementArg, Holder, makeTestId, styled, svg } from "grainjs";
|
import { Disposable, dom, DomElementArg, Holder, makeTestId, Observable, styled, svg } from "grainjs";
|
||||||
import { createPopper, Placement } from '@popperjs/core';
|
import { createPopper, Placement } from '@popperjs/core';
|
||||||
import { FocusLayer } from 'app/client/lib/FocusLayer';
|
import { FocusLayer } from 'app/client/lib/FocusLayer';
|
||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
@ -74,18 +74,34 @@ export interface IOnBoardingMsg {
|
|||||||
urlState?: IGristUrlState;
|
urlState?: IGristUrlState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _isTourActiveObs: Observable<boolean>|undefined;
|
||||||
|
|
||||||
|
// Returns a singleton observable for whether some tour is currently active.
|
||||||
|
//
|
||||||
|
// GristDoc subscribes to this observable in order to temporarily disable tips and other
|
||||||
|
// in-product popups from being shown while a tour is active.
|
||||||
|
export function isTourActiveObs(): Observable<boolean> {
|
||||||
|
if (!_isTourActiveObs) {
|
||||||
|
const obs = Observable.create<boolean>(null, false);
|
||||||
|
_isTourActiveObs = obs;
|
||||||
|
}
|
||||||
|
return _isTourActiveObs;
|
||||||
|
}
|
||||||
|
|
||||||
// There should only be one tour at a time. Use a holder to dispose the previous tour when
|
// There should only be one tour at a time. Use a holder to dispose the previous tour when
|
||||||
// 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: (lastMessageIndex: number) => 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.onDispose(() => isTourActiveObs().set(false));
|
||||||
ctl.start().catch(reportError);
|
ctl.start().catch(reportError);
|
||||||
|
isTourActiveObs().set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns whether some tour is currently active.
|
// Returns whether some tour is currently active.
|
||||||
export function isTourActive(): boolean {
|
export function isTourActive(): boolean {
|
||||||
return !tourSingleton.isEmpty();
|
return isTourActiveObs().get();
|
||||||
}
|
}
|
||||||
|
|
||||||
class OnBoardingError extends Error {
|
class OnBoardingError extends Error {
|
||||||
|
@ -337,7 +337,7 @@ export class PageWidgetSelect extends Disposable {
|
|||||||
cssIcon('TypeTable'), 'New Table',
|
cssIcon('TypeTable'), 'New Table',
|
||||||
// prevent the selection of 'New Table' if it is disabled
|
// prevent the selection of 'New Table' if it is disabled
|
||||||
dom.on('click', (ev) => !this._isNewTableDisabled.get() && this._selectTable('New Table')),
|
dom.on('click', (ev) => !this._isNewTableDisabled.get() && this._selectTable('New Table')),
|
||||||
this._behavioralPromptsManager.attachTip('pageWidgetPicker', {
|
this._behavioralPromptsManager.attachPopup('pageWidgetPicker', {
|
||||||
popupOptions: {
|
popupOptions: {
|
||||||
attach: null,
|
attach: null,
|
||||||
placement: 'right-start',
|
placement: 'right-start',
|
||||||
@ -395,7 +395,7 @@ export class PageWidgetSelect extends Disposable {
|
|||||||
),
|
),
|
||||||
'selectBy',
|
'selectBy',
|
||||||
{popupOptions: {attach: null}, domArgs: [
|
{popupOptions: {attach: null}, domArgs: [
|
||||||
this._behavioralPromptsManager.attachTip('pageWidgetPickerSelectBy', {
|
this._behavioralPromptsManager.attachPopup('pageWidgetPickerSelectBy', {
|
||||||
popupOptions: {
|
popupOptions: {
|
||||||
attach: null,
|
attach: null,
|
||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
|
@ -1013,16 +1013,7 @@ export class RightPanel extends Disposable {
|
|||||||
|
|
||||||
return domAsync(imports.loadViewPane().then(() => buildConfigContainer(cssSection(
|
return domAsync(imports.loadViewPane().then(() => buildConfigContainer(cssSection(
|
||||||
// Field config.
|
// Field config.
|
||||||
dom.maybeOwned(selectedField, (scope, field) => {
|
dom.maybe(selectedField, (field) => {
|
||||||
const requiredField = field.widgetOptionsJson.prop('formRequired');
|
|
||||||
// V2 thing.
|
|
||||||
// const hiddenField = field.widgetOptionsJson.prop('formHidden');
|
|
||||||
const defaultField = field.widgetOptionsJson.prop('formDefault');
|
|
||||||
const toComputed = (obs: typeof defaultField) => {
|
|
||||||
const result = Computed.create(scope, (use) => use(obs));
|
|
||||||
result.onWrite(val => obs.setAndSave(val));
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
const fieldTitle = field.widgetOptionsJson.prop('question');
|
const fieldTitle = field.widgetOptionsJson.prop('question');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -1063,21 +1054,10 @@ export class RightPanel extends Disposable {
|
|||||||
// cssSection(
|
// cssSection(
|
||||||
// builder.buildSelectWidgetDom(),
|
// builder.buildSelectWidgetDom(),
|
||||||
// ),
|
// ),
|
||||||
dom.maybe(use => ['Choice', 'ChoiceList', 'Ref', 'RefList'].includes(use(builder.field.pureType)), () => [
|
cssSection(
|
||||||
cssSection(
|
builder.buildFormConfigDom(),
|
||||||
builder.buildConfigDom(),
|
),
|
||||||
),
|
|
||||||
]),
|
|
||||||
]),
|
]),
|
||||||
cssSeparator(),
|
|
||||||
cssLabel(t("Field rules")),
|
|
||||||
cssRow(labeledSquareCheckbox(
|
|
||||||
toComputed(requiredField),
|
|
||||||
t("Required field"),
|
|
||||||
testId('field-required'),
|
|
||||||
)),
|
|
||||||
// V2 thing
|
|
||||||
// cssRow(labeledSquareCheckbox(toComputed(hiddenField), t("Hidden field")),),
|
|
||||||
];
|
];
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import {makeT} from 'app/client/lib/localization';
|
||||||
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||||
import {AppModel} from 'app/client/models/AppModel';
|
import {AppModel} from 'app/client/models/AppModel';
|
||||||
import {bigBasicButton, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
|
import {bigBasicButton, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
|
||||||
@ -7,7 +8,6 @@ import {cardPopup, cssPopupBody, cssPopupButtons, cssPopupCloseButton,
|
|||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {getGristConfig} from 'app/common/urlUtils';
|
import {getGristConfig} from 'app/common/urlUtils';
|
||||||
import {dom, styled} from 'grainjs';
|
import {dom, styled} from 'grainjs';
|
||||||
import { makeT } from '../lib/localization';
|
|
||||||
|
|
||||||
const t = makeT('WelcomeCoachingCall');
|
const t = makeT('WelcomeCoachingCall');
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ export function shouldShowWelcomeCoachingCall(appModel: AppModel) {
|
|||||||
|
|
||||||
// Defer showing coaching call until Add New tip is dismissed.
|
// Defer showing coaching call until Add New tip is dismissed.
|
||||||
const {behavioralPromptsManager, dismissedWelcomePopups} = appModel;
|
const {behavioralPromptsManager, dismissedWelcomePopups} = appModel;
|
||||||
if (behavioralPromptsManager.shouldShowTip('addNew')) { return false; }
|
if (behavioralPromptsManager.shouldShowPopup('addNew')) { return false; }
|
||||||
|
|
||||||
const popup = dismissedWelcomePopups.get().find(p => p.id === 'coachingCall');
|
const popup = dismissedWelcomePopups.get().find(p => p.id === 'coachingCall');
|
||||||
return (
|
return (
|
||||||
|
@ -7,9 +7,9 @@ import {pagePanels} from 'app/client/ui/PagePanels';
|
|||||||
import {setUpPage} from 'app/client/ui/setUpPage';
|
import {setUpPage} from 'app/client/ui/setUpPage';
|
||||||
import {createTopBarHome} from 'app/client/ui/TopBar';
|
import {createTopBarHome} from 'app/client/ui/TopBar';
|
||||||
import {bigBasicButtonLink, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
|
import {bigBasicButtonLink, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
|
||||||
import {colors, theme, vars} from 'app/client/ui2018/cssVars';
|
import {colors, mediaSmall, theme, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {icon} from 'app/client/ui2018/icons';
|
import {icon} from 'app/client/ui2018/icons';
|
||||||
import {getPageTitleSuffix} from 'app/common/gristUrls';
|
import {commonUrls, getPageTitleSuffix} from 'app/common/gristUrls';
|
||||||
import {getGristConfig} from 'app/common/urlUtils';
|
import {getGristConfig} from 'app/common/urlUtils';
|
||||||
import {dom, DomElementArg, makeTestId, observable, styled} from 'grainjs';
|
import {dom, DomElementArg, makeTestId, observable, styled} from 'grainjs';
|
||||||
|
|
||||||
@ -128,14 +128,14 @@ export function createFormNotFoundPage(message?: string) {
|
|||||||
cssFormErrorFooter(
|
cssFormErrorFooter(
|
||||||
cssFormPoweredByGrist(
|
cssFormPoweredByGrist(
|
||||||
cssFormPoweredByGristLink(
|
cssFormPoweredByGristLink(
|
||||||
{href: 'https://www.getgrist.com', target: '_blank'},
|
{href: commonUrls.forms, target: '_blank'},
|
||||||
t('Powered by'),
|
t('Powered by'),
|
||||||
cssGristLogo(),
|
cssGristLogo(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
cssFormBuildForm(
|
cssFormBuildForm(
|
||||||
cssFormBuildFormLink(
|
cssFormBuildFormLink(
|
||||||
{href: 'https://www.getgrist.com', target: '_blank'},
|
{href: commonUrls.forms, target: '_blank'},
|
||||||
t('Build your own form'),
|
t('Build your own form'),
|
||||||
icon('Expand'),
|
icon('Expand'),
|
||||||
),
|
),
|
||||||
@ -227,10 +227,17 @@ const cssButtonWrap = styled('div', `
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
const cssFormErrorPage = styled('div', `
|
const cssFormErrorPage = styled('div', `
|
||||||
--grist-form-padding: 48px;
|
background-color: ${colors.lightGrey};
|
||||||
min-height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-top: 52px;
|
padding: 52px 0px 52px 0px;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
@media ${mediaSmall} {
|
||||||
|
& {
|
||||||
|
padding: 20px 0px 20px 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssFormErrorContainer = styled('div', `
|
const cssFormErrorContainer = styled('div', `
|
||||||
@ -243,6 +250,7 @@ const cssFormError = styled('div', `
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
background-color: white;
|
||||||
border: 1px solid ${colors.darkGrey};
|
border: 1px solid ${colors.darkGrey};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
@ -254,8 +262,10 @@ const cssFormErrorBody = styled('div', `
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
const cssFormErrorImage = styled('img', `
|
const cssFormErrorImage = styled('img', `
|
||||||
width: 250px;
|
width: 100%;
|
||||||
height: 281px;
|
height: 100%;
|
||||||
|
max-width: 250px;
|
||||||
|
max-height: 281px;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssFormErrorText = styled('div', `
|
const cssFormErrorText = styled('div', `
|
||||||
@ -282,8 +292,6 @@ const cssFormPoweredByGrist = styled('div', `
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0px 10px;
|
padding: 0px 10px;
|
||||||
margin-left: calc(-1 * var(--grist-form-padding));
|
|
||||||
margin-right: calc(-1 * var(--grist-form-padding));
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssFormPoweredByGristLink = styled('a', `
|
const cssFormPoweredByGristLink = styled('a', `
|
||||||
|
@ -45,4 +45,12 @@ AbstractWidget.prototype.buildColorConfigDom = function(gristDoc) {
|
|||||||
return dom.create(CellStyle, this.field, gristDoc, this.defaultTextColor);
|
return dom.create(CellStyle, this.field, gristDoc, this.defaultTextColor);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AbstractWidget.prototype.buildFormConfigDom = function() {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
AbstractWidget.prototype.buildFormTransformConfigDom = function() {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = AbstractWidget;
|
module.exports = AbstractWidget;
|
||||||
|
@ -13,7 +13,7 @@ import { SingleCell } from 'app/common/TableData';
|
|||||||
import {KoSaveableObservable} from 'app/client/models/modelUtil';
|
import {KoSaveableObservable} from 'app/client/models/modelUtil';
|
||||||
import {UploadResult} from 'app/common/uploads';
|
import {UploadResult} from 'app/common/uploads';
|
||||||
import { GristObjCode } from 'app/plugin/GristData';
|
import { GristObjCode } from 'app/plugin/GristData';
|
||||||
import {Computed, dom, fromKo, input, onElem, styled} from 'grainjs';
|
import {Computed, dom, DomContents, fromKo, input, onElem, styled} from 'grainjs';
|
||||||
import {extname} from 'path';
|
import {extname} from 'path';
|
||||||
|
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ export class AttachmentsWidget extends NewAbstractWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildConfigDom(): Element {
|
public buildConfigDom(): DomContents {
|
||||||
const options = this.field.config.options;
|
const options = this.field.config.options;
|
||||||
const height = options.prop('height');
|
const height = options.prop('height');
|
||||||
const inputRange = input(
|
const inputRange = input(
|
||||||
|
@ -9,8 +9,7 @@ import {icon} from 'app/client/ui2018/icons';
|
|||||||
import {ChoiceListEntry} from 'app/client/widgets/ChoiceListEntry';
|
import {ChoiceListEntry} from 'app/client/widgets/ChoiceListEntry';
|
||||||
import {choiceToken, DEFAULT_BACKGROUND_COLOR, DEFAULT_COLOR} from 'app/client/widgets/ChoiceToken';
|
import {choiceToken, DEFAULT_BACKGROUND_COLOR, DEFAULT_COLOR} from 'app/client/widgets/ChoiceToken';
|
||||||
import {NTextBox} from 'app/client/widgets/NTextBox';
|
import {NTextBox} from 'app/client/widgets/NTextBox';
|
||||||
import {WidgetType} from 'app/common/widgetTypes';
|
import {Computed, dom, styled} from 'grainjs';
|
||||||
import {Computed, dom, styled, UseCB} from 'grainjs';
|
|
||||||
|
|
||||||
export type IChoiceOptions = Style
|
export type IChoiceOptions = Style
|
||||||
export type ChoiceOptions = Record<string, IChoiceOptions | undefined>;
|
export type ChoiceOptions = Record<string, IChoiceOptions | undefined>;
|
||||||
@ -75,37 +74,9 @@ export class ChoiceTextBox extends NTextBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public buildConfigDom() {
|
public buildConfigDom() {
|
||||||
const disabled = Computed.create(null,
|
|
||||||
use => use(this.field.disableModify)
|
|
||||||
|| use(use(this.field.column).disableEditData)
|
|
||||||
|| use(this.field.config.options.disabled('choices'))
|
|
||||||
);
|
|
||||||
|
|
||||||
const mixed = Computed.create(null,
|
|
||||||
use => !use(disabled)
|
|
||||||
&& (use(this.field.config.options.mixed('choices')) || use(this.field.config.options.mixed('choiceOptions')))
|
|
||||||
);
|
|
||||||
|
|
||||||
// If we are on forms, we don't want to show alignment options.
|
|
||||||
const notForm = (use: UseCB) => {
|
|
||||||
return use(use(this.field.viewSection).parentKey) !== WidgetType.Form;
|
|
||||||
};
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
dom.maybe(notForm, () => super.buildConfigDom()),
|
super.buildConfigDom(),
|
||||||
cssLabel(t('CHOICES')),
|
this._buildChoicesConfigDom(),
|
||||||
cssRow(
|
|
||||||
dom.autoDispose(disabled),
|
|
||||||
dom.autoDispose(mixed),
|
|
||||||
dom.create(
|
|
||||||
ChoiceListEntry,
|
|
||||||
this._choiceValues,
|
|
||||||
this._choiceOptionsByName,
|
|
||||||
this.save.bind(this),
|
|
||||||
disabled,
|
|
||||||
mixed
|
|
||||||
)
|
|
||||||
)
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,6 +84,19 @@ export class ChoiceTextBox extends NTextBox {
|
|||||||
return this.buildConfigDom();
|
return this.buildConfigDom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public buildFormConfigDom() {
|
||||||
|
return [
|
||||||
|
this._buildChoicesConfigDom(),
|
||||||
|
super.buildFormConfigDom(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildFormTransformConfigDom() {
|
||||||
|
return [
|
||||||
|
this._buildChoicesConfigDom(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected getChoiceValuesSet(): Computed<Set<string>> {
|
protected getChoiceValuesSet(): Computed<Set<string>> {
|
||||||
return this._choiceValuesSet;
|
return this._choiceValuesSet;
|
||||||
}
|
}
|
||||||
@ -128,6 +112,35 @@ export class ChoiceTextBox extends NTextBox {
|
|||||||
};
|
};
|
||||||
return this.field.config.updateChoices(renames, options);
|
return this.field.config.updateChoices(renames, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _buildChoicesConfigDom() {
|
||||||
|
const disabled = Computed.create(null,
|
||||||
|
use => use(this.field.disableModify)
|
||||||
|
|| use(use(this.field.column).disableEditData)
|
||||||
|
|| use(this.field.config.options.disabled('choices'))
|
||||||
|
);
|
||||||
|
|
||||||
|
const mixed = Computed.create(null,
|
||||||
|
use => !use(disabled)
|
||||||
|
&& (use(this.field.config.options.mixed('choices')) || use(this.field.config.options.mixed('choiceOptions')))
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
cssLabel(t('CHOICES')),
|
||||||
|
cssRow(
|
||||||
|
dom.autoDispose(disabled),
|
||||||
|
dom.autoDispose(mixed),
|
||||||
|
dom.create(
|
||||||
|
ChoiceListEntry,
|
||||||
|
this._choiceValues,
|
||||||
|
this._choiceOptionsByName,
|
||||||
|
this.save.bind(this),
|
||||||
|
disabled,
|
||||||
|
mixed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts a POJO containing choice options to an ES6 Map
|
// Converts a POJO containing choice options to an ES6 Map
|
||||||
|
@ -6,11 +6,12 @@ var kd = require('../lib/koDom');
|
|||||||
var kf = require('../lib/koForm');
|
var kf = require('../lib/koForm');
|
||||||
var AbstractWidget = require('./AbstractWidget');
|
var AbstractWidget = require('./AbstractWidget');
|
||||||
|
|
||||||
|
const {FieldRulesConfig} = require('app/client/components/Forms/FormConfig');
|
||||||
const {fromKoSave} = require('app/client/lib/fromKoSave');
|
const {fromKoSave} = require('app/client/lib/fromKoSave');
|
||||||
const {alignmentSelect, cssButtonSelect} = require('app/client/ui2018/buttonSelect');
|
const {alignmentSelect, cssButtonSelect} = require('app/client/ui2018/buttonSelect');
|
||||||
const {cssRow, cssLabel} = require('app/client/ui/RightPanelStyles');
|
const {cssLabel, cssRow} = require('app/client/ui/RightPanelStyles');
|
||||||
const {cssTextInput} = require("app/client/ui2018/editableLabel");
|
const {cssTextInput} = require("app/client/ui2018/editableLabel");
|
||||||
const {styled, fromKo} = require('grainjs');
|
const {dom: gdom, styled, fromKo} = require('grainjs');
|
||||||
const {select} = require('app/client/ui2018/menus');
|
const {select} = require('app/client/ui2018/menus');
|
||||||
const {dateFormatOptions} = require('app/common/parseDate');
|
const {dateFormatOptions} = require('app/common/parseDate');
|
||||||
|
|
||||||
@ -79,6 +80,12 @@ DateTextBox.prototype.buildTransformConfigDom = function() {
|
|||||||
return this.buildDateConfigDom();
|
return this.buildDateConfigDom();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DateTextBox.prototype.buildFormConfigDom = function() {
|
||||||
|
return [
|
||||||
|
gdom.create(FieldRulesConfig, this.field),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
DateTextBox.prototype.buildDom = function(row) {
|
DateTextBox.prototype.buildDom = function(row) {
|
||||||
let value = row[this.field.colId()];
|
let value = row[this.field.colId()];
|
||||||
return dom('div.field_clip',
|
return dom('div.field_clip',
|
||||||
|
@ -305,7 +305,7 @@ export class FieldBuilder extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (op.label === 'Reference') {
|
if (op.label === 'Reference') {
|
||||||
return this.gristDoc.behavioralPromptsManager.attachTip('referenceColumns', {
|
return this.gristDoc.behavioralPromptsManager.attachPopup('referenceColumns', {
|
||||||
popupOptions: {
|
popupOptions: {
|
||||||
attach: `.${cssTypeSelectMenu.className}`,
|
attach: `.${cssTypeSelectMenu.className}`,
|
||||||
placement: 'left-start',
|
placement: 'left-start',
|
||||||
@ -412,7 +412,7 @@ export class FieldBuilder extends Disposable {
|
|||||||
return [
|
return [
|
||||||
cssLabel(t('DATA FROM TABLE'),
|
cssLabel(t('DATA FROM TABLE'),
|
||||||
kd.maybe(this._showRefConfigPopup, () => {
|
kd.maybe(this._showRefConfigPopup, () => {
|
||||||
return dom('div', this.gristDoc.behavioralPromptsManager.attachTip(
|
return dom('div', this.gristDoc.behavioralPromptsManager.attachPopup(
|
||||||
'referenceColumnsConfig',
|
'referenceColumnsConfig',
|
||||||
{
|
{
|
||||||
onDispose: () => this._showRefConfigPopup(false),
|
onDispose: () => this._showRefConfigPopup(false),
|
||||||
@ -501,6 +501,14 @@ export class FieldBuilder extends Disposable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public buildFormConfigDom() {
|
||||||
|
return dom('div',
|
||||||
|
kd.maybe(() => !this._isTransformingType() && this.widgetImpl(), (widget: NewAbstractWidget) =>
|
||||||
|
dom('div', widget.buildFormConfigDom())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the FieldBuilder Options Config DOM. Calls the buildConfigDom function of its widgetImpl.
|
* Builds the FieldBuilder Options Config DOM. Calls the buildConfigDom function of its widgetImpl.
|
||||||
*/
|
*/
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { FieldRulesConfig } from 'app/client/components/Forms/FormConfig';
|
||||||
import { fromKoSave } from 'app/client/lib/fromKoSave';
|
import { fromKoSave } from 'app/client/lib/fromKoSave';
|
||||||
import { DataRowModel } from 'app/client/models/DataRowModel';
|
import { DataRowModel } from 'app/client/models/DataRowModel';
|
||||||
import { ViewFieldRec } from 'app/client/models/entities/ViewFieldRec';
|
import { ViewFieldRec } from 'app/client/models/entities/ViewFieldRec';
|
||||||
@ -51,10 +52,16 @@ export class NTextBox extends NewAbstractWidget {
|
|||||||
[{value: true, icon: 'Wrap'}],
|
[{value: true, icon: 'Wrap'}],
|
||||||
toggle,
|
toggle,
|
||||||
cssButtonSelect.cls('-disabled', wrapDisabled),
|
cssButtonSelect.cls('-disabled', wrapDisabled),
|
||||||
),
|
),
|
||||||
testId('tb-wrap-text')
|
testId('tb-wrap-text'),
|
||||||
)
|
),
|
||||||
)
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildFormConfigDom(): DomContents {
|
||||||
|
return [
|
||||||
|
dom.create(FieldRulesConfig, this.field),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +76,14 @@ export abstract class NewAbstractWidget extends Disposable {
|
|||||||
return dom.create(CellStyle, this.field, gristDoc, this.defaultTextColor);
|
return dom.create(CellStyle, this.field, gristDoc, this.defaultTextColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public buildFormConfigDom(): DomContents {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildFormTransformConfigDom(): DomContents {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the data cell DOM.
|
* Builds the data cell DOM.
|
||||||
* @param {DataRowModel} row - The rowModel object.
|
* @param {DataRowModel} row - The rowModel object.
|
||||||
|
@ -9,9 +9,8 @@ import {icon} from 'app/client/ui2018/icons';
|
|||||||
import {IOptionFull, select} from 'app/client/ui2018/menus';
|
import {IOptionFull, select} from 'app/client/ui2018/menus';
|
||||||
import {NTextBox} from 'app/client/widgets/NTextBox';
|
import {NTextBox} from 'app/client/widgets/NTextBox';
|
||||||
import {isFullReferencingType, isVersions} from 'app/common/gristTypes';
|
import {isFullReferencingType, isVersions} from 'app/common/gristTypes';
|
||||||
import {WidgetType} from 'app/common/widgetTypes';
|
|
||||||
import {UIRowId} from 'app/plugin/GristAPI';
|
import {UIRowId} from 'app/plugin/GristAPI';
|
||||||
import {Computed, dom, styled, UseCB} from 'grainjs';
|
import {Computed, dom, styled} from 'grainjs';
|
||||||
|
|
||||||
|
|
||||||
const t = makeT('Reference');
|
const t = makeT('Reference');
|
||||||
@ -49,16 +48,10 @@ export class Reference extends NTextBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public buildConfigDom() {
|
public buildConfigDom() {
|
||||||
// If we are on forms, we don't want to show alignment options.
|
|
||||||
const notForm = (use: UseCB) => {
|
|
||||||
return use(use(this.field.viewSection).parentKey) !== WidgetType.Form;
|
|
||||||
};
|
|
||||||
return [
|
return [
|
||||||
this.buildTransformConfigDom(),
|
this.buildTransformConfigDom(),
|
||||||
dom.maybe(notForm, () => [
|
cssLabel(t('CELL FORMAT')),
|
||||||
cssLabel(t('CELL FORMAT')),
|
super.buildConfigDom(),
|
||||||
super.buildConfigDom()
|
|
||||||
])
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,6 +69,17 @@ export class Reference extends NTextBox {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public buildFormConfigDom() {
|
||||||
|
return [
|
||||||
|
this.buildTransformConfigDom(),
|
||||||
|
super.buildFormConfigDom(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildFormTransformConfigDom() {
|
||||||
|
return this.buildTransformConfigDom();
|
||||||
|
}
|
||||||
|
|
||||||
public buildDom(row: DataRowModel) {
|
public buildDom(row: DataRowModel) {
|
||||||
// Note: we require 2 observables here because changes to the cell value (reference id)
|
// Note: we require 2 observables here because changes to the cell value (reference id)
|
||||||
// and the display value (display column) are not bundled. This can cause `formattedValue`
|
// and the display value (display column) are not bundled. This can cause `formattedValue`
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
import * as commands from 'app/client/components/commands';
|
import * as commands from 'app/client/components/commands';
|
||||||
|
import { FieldRulesConfig } from 'app/client/components/Forms/FormConfig';
|
||||||
import { DataRowModel } from 'app/client/models/DataRowModel';
|
import { DataRowModel } from 'app/client/models/DataRowModel';
|
||||||
import { ViewFieldRec } from 'app/client/models/entities/ViewFieldRec';
|
import { ViewFieldRec } from 'app/client/models/entities/ViewFieldRec';
|
||||||
import { KoSaveableObservable } from 'app/client/models/modelUtil';
|
import { KoSaveableObservable } from 'app/client/models/modelUtil';
|
||||||
import { NewAbstractWidget, Options } from 'app/client/widgets/NewAbstractWidget';
|
import { NewAbstractWidget, Options } from 'app/client/widgets/NewAbstractWidget';
|
||||||
import { theme } from 'app/client/ui2018/cssVars';
|
import { theme } from 'app/client/ui2018/cssVars';
|
||||||
import { dom } from 'grainjs';
|
import { dom, DomContents } from 'grainjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ToggleBase - The base class for toggle widgets, such as a checkbox or a switch.
|
* ToggleBase - The base class for toggle widgets, such as a checkbox or a switch.
|
||||||
*/
|
*/
|
||||||
abstract class ToggleBase extends NewAbstractWidget {
|
abstract class ToggleBase extends NewAbstractWidget {
|
||||||
|
public buildFormConfigDom(): DomContents {
|
||||||
|
return [
|
||||||
|
dom.create(FieldRulesConfig, this.field),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected _addClickEventHandlers(row: DataRowModel) {
|
protected _addClickEventHandlers(row: DataRowModel) {
|
||||||
return [
|
return [
|
||||||
dom.on('click', (event) => {
|
dom.on('click', (event) => {
|
||||||
|
@ -286,7 +286,7 @@ class Bool extends BaseQuestion {
|
|||||||
const label = field.question ? field.question : field.colId;
|
const label = field.question ? field.question : field.colId;
|
||||||
return `
|
return `
|
||||||
<label class='grist-switch ${requiredLabel}'>
|
<label class='grist-switch ${requiredLabel}'>
|
||||||
<input type='checkbox' name='${this.name(field)}' value="1" ${required} />
|
<input type='checkbox' name='${this.name(field)}' value="1" ${required} />
|
||||||
<div class="grist-widget_switch grist-switch_transition">
|
<div class="grist-widget_switch grist-switch_transition">
|
||||||
<div class="grist-switch_slider"></div>
|
<div class="grist-switch_slider"></div>
|
||||||
<div class="grist-switch_circle"></div>
|
<div class="grist-switch_circle"></div>
|
||||||
|
@ -88,6 +88,7 @@ export const BehavioralPrompt = StringUnion(
|
|||||||
'rickRow',
|
'rickRow',
|
||||||
'customURL',
|
'customURL',
|
||||||
'calendarConfig',
|
'calendarConfig',
|
||||||
|
'formsAreHere',
|
||||||
);
|
);
|
||||||
export type BehavioralPrompt = typeof BehavioralPrompt.type;
|
export type BehavioralPrompt = typeof BehavioralPrompt.type;
|
||||||
|
|
||||||
|
@ -94,6 +94,7 @@ export const commonUrls = {
|
|||||||
functions: 'https://support.getgrist.com/functions',
|
functions: 'https://support.getgrist.com/functions',
|
||||||
formulaSheet: 'https://support.getgrist.com/formula-cheat-sheet',
|
formulaSheet: 'https://support.getgrist.com/formula-cheat-sheet',
|
||||||
formulas: 'https://support.getgrist.com/formulas',
|
formulas: 'https://support.getgrist.com/formulas',
|
||||||
|
forms: 'https://www.getgrist.com/forms/?utm_source=grist-forms&utm_medium=grist-forms&utm_campaign=forms-footer',
|
||||||
|
|
||||||
basicTutorial: 'https://templates.getgrist.com/woXtXUBmiN5T/Grist-Basics',
|
basicTutorial: 'https://templates.getgrist.com/woXtXUBmiN5T/Grist-Basics',
|
||||||
basicTutorialImage: 'https://www.getgrist.com/wp-content/uploads/2021/08/lightweight-crm.png',
|
basicTutorialImage: 'https://www.getgrist.com/wp-content/uploads/2021/08/lightweight-crm.png',
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
import {DocData} from 'app/common/DocData';
|
import {DocData} from 'app/common/DocData';
|
||||||
import {extractTypeFromColType, isRaisedException} from "app/common/gristTypes";
|
import {extractTypeFromColType, isRaisedException} from "app/common/gristTypes";
|
||||||
import {Box, BoxType, FieldModel, INITIAL_FIELDS_COUNT, RenderBox, RenderContext} from "app/common/Forms";
|
import {Box, BoxType, FieldModel, INITIAL_FIELDS_COUNT, RenderBox, RenderContext} from "app/common/Forms";
|
||||||
import {buildUrlId, parseUrlId, SHARE_KEY_PREFIX} from "app/common/gristUrls";
|
import {buildUrlId, commonUrls, parseUrlId, SHARE_KEY_PREFIX} from "app/common/gristUrls";
|
||||||
import {isAffirmative, safeJsonParse, timeoutReached} from "app/common/gutil";
|
import {isAffirmative, safeJsonParse, timeoutReached} from "app/common/gutil";
|
||||||
import {SchemaTypes} from "app/common/schema";
|
import {SchemaTypes} from "app/common/schema";
|
||||||
import {SortFunc} from 'app/common/SortFunc';
|
import {SortFunc} from 'app/common/SortFunc';
|
||||||
@ -1538,7 +1538,8 @@ export class DocWorkerApi {
|
|||||||
CONTENT: html,
|
CONTENT: html,
|
||||||
SUCCESS_TEXT: box.successText || 'Thank you! Your response has been recorded.',
|
SUCCESS_TEXT: box.successText || 'Thank you! Your response has been recorded.',
|
||||||
SUCCESS_URL: redirectUrl,
|
SUCCESS_URL: redirectUrl,
|
||||||
TITLE: `${section.title || tableName || tableId || 'Form'} - Grist`
|
TITLE: `${section.title || tableName || tableId || 'Form'} - Grist`,
|
||||||
|
FORMS_LANDING_PAGE_URL: commonUrls.forms,
|
||||||
});
|
});
|
||||||
this._grist.getTelemetry().logEvent(req, 'visitedForm', {
|
this._grist.getTelemetry().logEvent(req, 'visitedForm', {
|
||||||
full: {
|
full: {
|
||||||
|
@ -24,7 +24,7 @@ body {
|
|||||||
background-color: #f7f7f7;
|
background-color: #f7f7f7;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-top: 52px;
|
padding: 52px 0px 52px 0px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Liberation Sans", Helvetica, Arial, sans-serif,
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Liberation Sans", Helvetica, Arial, sans-serif,
|
||||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
@ -61,7 +61,7 @@ body {
|
|||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
.grist-form-container {
|
.grist-form-container {
|
||||||
padding-top: 20px;
|
padding: 20px 0px 20px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grist-form {
|
.grist-form {
|
||||||
@ -89,6 +89,7 @@ body {
|
|||||||
.grist-form input[type="date"],
|
.grist-form input[type="date"],
|
||||||
.grist-form input[type="datetime-local"],
|
.grist-form input[type="datetime-local"],
|
||||||
.grist-form input[type="number"] {
|
.grist-form input[type="number"] {
|
||||||
|
height: 27px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border: 1px solid var(--dark-gray);
|
border: 1px solid var(--dark-gray);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
@ -164,6 +165,7 @@ body {
|
|||||||
outline-width: 1px;
|
outline-width: 1px;
|
||||||
background: white;
|
background: white;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
|
height: 27px;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -249,7 +251,6 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grist-power-by {
|
.grist-power-by {
|
||||||
margin-top: 24px;
|
|
||||||
color: #494949;
|
color: #494949;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@ -258,10 +259,8 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-top: 1px solid var(--dark-gray);
|
padding-left: 10px;
|
||||||
padding: 10px;
|
padding-right: 10px;
|
||||||
margin-left: calc(-1 * var(--grist-form-padding));
|
|
||||||
margin-right: calc(-1 * var(--grist-form-padding));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grist-power-by a {
|
.grist-power-by a {
|
||||||
@ -302,15 +301,6 @@ body {
|
|||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* When an empty value is selected, show the placeholder in italic gray.
|
|
||||||
* The css is: every select that has an empty option selected, and is not active (so not open).
|
|
||||||
*/
|
|
||||||
.grist-form select:has(option[value='']:checked):not(:active) {
|
|
||||||
font-style: italic;
|
|
||||||
color: var(--light-gray, #bfbfbf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Markdown reset */
|
/* Markdown reset */
|
||||||
|
|
||||||
.grist-form h1,
|
.grist-form h1,
|
||||||
@ -364,7 +354,7 @@ body {
|
|||||||
|
|
||||||
.grist-switch {
|
.grist-switch {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.grist-switch input[type='checkbox']::after {
|
.grist-switch input[type='checkbox']::after {
|
||||||
@ -451,8 +441,10 @@ input:checked + .grist-switch_transition > .grist-switch_circle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grist-form-confirm-image {
|
.grist-form-confirm-image {
|
||||||
width: 250px;
|
width: 100%;
|
||||||
height: 215px;
|
height: 100%;
|
||||||
|
max-width: 250px;
|
||||||
|
max-height: 215px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grist-form-confirm-text {
|
.grist-form-confirm-text {
|
||||||
@ -491,27 +483,29 @@ input:checked + .grist-switch_transition > .grist-switch_circle {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grist-form-footer,
|
||||||
.grist-form-confirm-footer {
|
.grist-form-confirm-footer {
|
||||||
border-top: 1px solid var(--dark-gray);
|
border-top: 1px solid var(--dark-gray);
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grist-form-footer {
|
||||||
|
margin-left: calc(-1 * var(--grist-form-padding));
|
||||||
|
margin-right: calc(-1 * var(--grist-form-padding));
|
||||||
|
}
|
||||||
|
|
||||||
|
.grist-form-confirm-footer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grist-form-confirm-footer .grist-power-by {
|
.grist-form-build-form-link-container {
|
||||||
margin-top: 0px;
|
|
||||||
padding-top: 0px;
|
|
||||||
padding-bottom: 0px;
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grist-form-confirm-build-form {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grist-form-confirm-build-form-link {
|
.grist-form-build-form-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -31,11 +31,19 @@
|
|||||||
data-grist-success-url="{{ SUCCESS_URL }}"
|
data-grist-success-url="{{ SUCCESS_URL }}"
|
||||||
>
|
>
|
||||||
{{ dompurify CONTENT }}
|
{{ dompurify CONTENT }}
|
||||||
<div class="grist-power-by">
|
<div class='grist-form-footer'>
|
||||||
<a href="https://getgrist.com" target="_blank">
|
<div class="grist-power-by">
|
||||||
<div>Powered by</div>
|
<a href="{{ FORMS_LANDING_PAGE_URL }}" target="_blank">
|
||||||
<div class="grist-logo"></div>
|
<div>Powered by</div>
|
||||||
|
<div class="grist-logo"></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class='grist-form-build-form-link-container'>
|
||||||
|
<a class='grist-form-build-form-link' href="{{ FORMS_LANDING_PAGE_URL }}" target="_blank">
|
||||||
|
Build your own form
|
||||||
|
<div class="grist-form-icon grist-form-icon-expand"></div>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -59,13 +67,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='grist-form-confirm-footer'>
|
<div class='grist-form-confirm-footer'>
|
||||||
<div class="grist-power-by">
|
<div class="grist-power-by">
|
||||||
<a href="https://www.getgrist.com" target="_blank">
|
<a href="https://www.getgrist.com/forms/?utm_source=grist-forms&utm_medium=grist-forms&utm_campaign=forms-footer" target="_blank">
|
||||||
<div>Powered by</div>
|
<div>Powered by</div>
|
||||||
<div class="grist-logo"></div>
|
<div class="grist-logo"></div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class='grist-form-confirm-build-form'>
|
<div class='grist-form-build-form-link-container'>
|
||||||
<a class='grist-form-confirm-build-form-link' href="https://www.getgrist.com" target="_blank">
|
<a class='grist-form-build-form-link' href="https://www.getgrist.com/forms/?utm_source=grist-forms&utm_medium=grist-forms&utm_campaign=forms-footer" target="_blank">
|
||||||
Build your own form
|
Build your own form
|
||||||
<div class="grist-form-icon grist-form-icon-expand"></div>
|
<div class="grist-form-icon grist-form-icon-expand"></div>
|
||||||
</a>
|
</a>
|
||||||
|
268
test/nbrowser/BehavioralPrompts.ts
Normal file
268
test/nbrowser/BehavioralPrompts.ts
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
import {assert, driver, Key} from 'mocha-webdriver';
|
||||||
|
import * as gu from 'test/nbrowser/gristUtils';
|
||||||
|
import {setupTestSuite} from 'test/nbrowser/testUtils';
|
||||||
|
|
||||||
|
describe('BehavioralPrompts', function() {
|
||||||
|
this.timeout(20000);
|
||||||
|
const cleanup = setupTestSuite();
|
||||||
|
|
||||||
|
let session: gu.Session;
|
||||||
|
let docId: string;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
session = await gu.session().user('user1').login({showTips: true});
|
||||||
|
await gu.dismissCoachingCall();
|
||||||
|
docId = await session.tempNewDoc(cleanup, 'BehavioralPrompts');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => gu.checkForErrors());
|
||||||
|
|
||||||
|
describe('when helpCenter is hidden', function() {
|
||||||
|
gu.withEnvironmentSnapshot({'GRIST_HIDE_UI_ELEMENTS': 'helpCenter'});
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
const sessionNoHelpCenter = await gu.session().user('user3').login({
|
||||||
|
isFirstLogin: false,
|
||||||
|
freshAccount: true,
|
||||||
|
showTips: true,
|
||||||
|
});
|
||||||
|
await gu.dismissCoachingCall();
|
||||||
|
await sessionNoHelpCenter.tempNewDoc(cleanup, 'BehavioralPromptsNoHelpCenter');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be shown', async function() {
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
await gu.toggleSidePanel('right', 'open');
|
||||||
|
await driver.find('.test-right-tab-field').click();
|
||||||
|
await driver.find('.test-fbuilder-type-select').click();
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show an announcement for forms', async function() {
|
||||||
|
await assertPromptTitle('Forms are here!');
|
||||||
|
await gu.dismissBehavioralPrompts();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when anonymous', function() {
|
||||||
|
before(async () => {
|
||||||
|
const anonymousSession = await gu.session().anon.login({
|
||||||
|
showTips: true,
|
||||||
|
});
|
||||||
|
await anonymousSession.loadDocMenu('/');
|
||||||
|
await driver.find('.test-intro-create-doc').click();
|
||||||
|
await gu.waitForDocToLoad();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not shown an announcement for forms', async function() {
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown when the column type select menu is opened', async function() {
|
||||||
|
await gu.toggleSidePanel('right', 'open');
|
||||||
|
await driver.find('.test-right-tab-field').click();
|
||||||
|
await driver.find('.test-fbuilder-type-select').click();
|
||||||
|
await assertPromptTitle('Reference Columns');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be temporarily dismissed on click-away', async function() {
|
||||||
|
await gu.getCell({col: 'A', rowNum: 1}).click();
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown again the next time the menu is opened', async function() {
|
||||||
|
await driver.find('.test-fbuilder-type-select').click();
|
||||||
|
await assertPromptTitle('Reference Columns');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be permanently dismissed when "Got it" is clicked', async function() {
|
||||||
|
await gu.dismissBehavioralPrompts();
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
|
||||||
|
// Refresh the page and make sure the prompt isn't shown again.
|
||||||
|
await session.loadDoc(`/doc/${docId}`);
|
||||||
|
await driver.find('.test-fbuilder-type-select').click();
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
await gu.sendKeys(Key.ESCAPE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown after selecting a reference column type', async function() {
|
||||||
|
await gu.setType(/Reference$/);
|
||||||
|
await assertPromptTitle('Reference Columns');
|
||||||
|
await gu.undo();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown after selecting a reference list column type', async function() {
|
||||||
|
await gu.setType(/Reference List$/);
|
||||||
|
await assertPromptTitle('Reference Columns');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown when opening the Raw Data page', async function() {
|
||||||
|
await driver.find('.test-tools-raw').click();
|
||||||
|
await assertPromptTitle('Raw Data page');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown when opening the Access Rules page', async function() {
|
||||||
|
await driver.find('.test-tools-access-rules').click();
|
||||||
|
await assertPromptTitle('Access Rules');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown when opening the filter menu', async function() {
|
||||||
|
await gu.openPage('Table1');
|
||||||
|
await gu.openColumnMenu('A', 'Filter');
|
||||||
|
await assertPromptTitle('Pinning Filters');
|
||||||
|
await gu.dismissBehavioralPrompts();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown when adding a second pinned filter', async function() {
|
||||||
|
await driver.find('.test-filter-menu-apply-btn').click();
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
await gu.openColumnMenu('B', 'Filter');
|
||||||
|
await driver.find('.test-filter-menu-apply-btn').click();
|
||||||
|
await assertPromptTitle('Nested Filtering');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown when opening the page widget picker', async function() {
|
||||||
|
await gu.openAddWidgetToPage();
|
||||||
|
await assertPromptTitle('Selecting Data');
|
||||||
|
await gu.dismissBehavioralPrompts();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown when select by is an available option', async function() {
|
||||||
|
await driver.findContent('.test-wselect-table', /Table1/).click();
|
||||||
|
await assertPromptTitle('Linking Widgets');
|
||||||
|
await gu.dismissBehavioralPrompts();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown when adding a card widget', async function() {
|
||||||
|
await gu.selectWidget('Card', /Table1/);
|
||||||
|
await assertPromptTitle('Editing Card Layout');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be shown when adding a non-card widget', async function() {
|
||||||
|
await gu.addNewPage('Table', /Table1/);
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown when adding a card list widget', async function() {
|
||||||
|
await gu.addNewPage('Card List', /Table1/);
|
||||||
|
await assertPromptTitle('Editing Card Layout');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown after adding custom view as a new page', async function() {
|
||||||
|
await gu.addNewPage('Custom', 'Table1');
|
||||||
|
await assertPromptTitle('Custom Widgets');
|
||||||
|
await gu.undo();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown after adding custom section', async function() {
|
||||||
|
await gu.addNewSection('Custom', 'Table1');
|
||||||
|
await assertPromptTitle('Custom Widgets');
|
||||||
|
await gu.undo();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('for the Add New button', function() {
|
||||||
|
it('should not be shown if site is empty', async function() {
|
||||||
|
session = await gu.session().user('user4').login({showTips: true});
|
||||||
|
await gu.dismissCoachingCall();
|
||||||
|
await driver.navigate().refresh();
|
||||||
|
await gu.loadDocMenu('/');
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be shown if site has documents', async function() {
|
||||||
|
await session.tempNewDoc(cleanup, 'BehavioralPromptsAddNew');
|
||||||
|
await session.loadDocMenu('/');
|
||||||
|
await assertPromptTitle('Add New');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be shown on the Trash page', async function() {
|
||||||
|
// Load /p/trash and check that tip isn't initially shown.
|
||||||
|
await session.loadDocMenu('/p/trash');
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only be shown once each visit to the doc menu', async function() {
|
||||||
|
// Navigate to another page without reloading; the tip should now be shown.
|
||||||
|
await driver.find('.test-dm-all-docs').click();
|
||||||
|
await gu.waitForDocMenuToLoad();
|
||||||
|
await assertPromptTitle('Add New');
|
||||||
|
|
||||||
|
// Navigate to another page; the tip should no longer be shown.
|
||||||
|
await driver.findContent('.test-dm-workspace', /Home/).click();
|
||||||
|
await gu.waitForDocMenuToLoad();
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should stop showing tips if "Don't show tips" is checked`, async function() {
|
||||||
|
// Log in as a new user who hasn't seen any tips yet.
|
||||||
|
session = await gu.session().user('user2').login({showTips: true});
|
||||||
|
docId = await session.tempNewDoc(cleanup, 'BehavioralPromptsDontShowTips');
|
||||||
|
await gu.loadDoc(`/doc/${docId}`);
|
||||||
|
|
||||||
|
// Check "Don't show tips" in the Reference Columns tip and dismiss it.
|
||||||
|
await gu.setType(/Reference$/);
|
||||||
|
await gu.scrollPanel(false);
|
||||||
|
await driver.findWait('.test-behavioral-prompt-dont-show-tips', 1000).click();
|
||||||
|
await gu.dismissBehavioralPrompts();
|
||||||
|
|
||||||
|
// Now visit Raw Data and check that its tip isn't shown.
|
||||||
|
await driver.find('.test-tools-raw').click();
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when welcome tour is active', function() {
|
||||||
|
before(async () => {
|
||||||
|
const welcomeTourSession = await gu.session().user('user3').login({
|
||||||
|
isFirstLogin: false,
|
||||||
|
freshAccount: true,
|
||||||
|
showTips: true,
|
||||||
|
});
|
||||||
|
await welcomeTourSession.tempNewDoc(cleanup, 'BehavioralPromptsWelcomeTour');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be shown', async function() {
|
||||||
|
assert.isTrue(await driver.find('.test-onboarding-close').isDisplayed());
|
||||||
|
// The forms announcement is normally shown here.
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when in a tutorial', function() {
|
||||||
|
gu.withEnvironmentSnapshot({'GRIST_UI_FEATURES': 'tutorials'});
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
const tutorialSession = await gu.session().user('user3').login({
|
||||||
|
showTips: true,
|
||||||
|
});
|
||||||
|
const doc = await tutorialSession.tempDoc(cleanup, 'DocTutorial.grist', {load: false});
|
||||||
|
const api = tutorialSession.createHomeApi();
|
||||||
|
await api.updateDoc(doc.id, {type: 'tutorial'});
|
||||||
|
await tutorialSession.loadDoc(`/doc/${doc.id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be shown', async function() {
|
||||||
|
// The forms announcement is normally shown here.
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
await driver.find('.test-floating-popup-minimize-maximize').click();
|
||||||
|
await gu.toggleSidePanel('right', 'open');
|
||||||
|
await driver.find('.test-right-tab-field').click();
|
||||||
|
await driver.find('.test-fbuilder-type-select').click();
|
||||||
|
await assertPromptTitle(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function assertPromptTitle(title: string | null) {
|
||||||
|
if (title === null) {
|
||||||
|
await gu.waitToPass(async () => {
|
||||||
|
assert.equal(await driver.find('.test-behavioral-prompt').isPresent(), false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await gu.waitToPass(async () => {
|
||||||
|
assert.equal(await driver.find('.test-behavioral-prompt-title').getText(), title);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -898,6 +898,20 @@ describe('FormView', function() {
|
|||||||
assert.deepEqual(await readLabels(), ['A', 'B', 'C', 'D']);
|
assert.deepEqual(await readLabels(), ['A', 'B', 'C', 'D']);
|
||||||
assert.equal(await element('Section', 1).element('label', 4).getText(), 'D');
|
assert.equal(await element('Section', 1).element('label', 4).getText(), 'D');
|
||||||
|
|
||||||
|
// Make sure that deleting the section also hides its fields and unmaps them.
|
||||||
|
await element('Section').element('Paragraph', 1).click();
|
||||||
|
await gu.sendKeys(Key.ESCAPE, Key.UP, Key.DELETE);
|
||||||
|
await gu.waitForServer();
|
||||||
|
assert.equal(await elementCount('Section'), 0);
|
||||||
|
assert.deepEqual(await readLabels(), []);
|
||||||
|
await gu.openWidgetPanel();
|
||||||
|
assert.deepEqual(await hiddenColumns(), ['A', 'B', 'C', 'Choice', 'D']);
|
||||||
|
|
||||||
|
await gu.undo();
|
||||||
|
assert.equal(await elementCount('Section'), 1);
|
||||||
|
assert.deepEqual(await readLabels(), ['A', 'B', 'C', 'D']);
|
||||||
|
assert.deepEqual(await hiddenColumns(), ['Choice']);
|
||||||
|
|
||||||
await revert();
|
await revert();
|
||||||
assert.deepEqual(await readLabels(), ['A', 'B', 'C']);
|
assert.deepEqual(await readLabels(), ['A', 'B', 'C']);
|
||||||
});
|
});
|
||||||
@ -981,6 +995,31 @@ describe('FormView', function() {
|
|||||||
assert.equal(await element('column', 2).element('label').getText(), 'D');
|
assert.equal(await element('column', 2).element('label').getText(), 'D');
|
||||||
assert.equal(await element('column', 3).type(), 'Placeholder');
|
assert.equal(await element('column', 3).type(), 'Placeholder');
|
||||||
|
|
||||||
|
// Add a second question column.
|
||||||
|
await element('Columns').element(`Placeholder`, 1).click();
|
||||||
|
await clickMenu('Text');
|
||||||
|
await gu.waitForServer();
|
||||||
|
|
||||||
|
// Delete the column and make sure both questions get deleted.
|
||||||
|
await element('Columns').element('Field', 1).click();
|
||||||
|
await gu.sendKeys(Key.ESCAPE, Key.UP, Key.DELETE);
|
||||||
|
await gu.waitForServer();
|
||||||
|
assert.deepEqual(await readLabels(), ['A', 'B', 'C']);
|
||||||
|
await gu.openWidgetPanel();
|
||||||
|
assert.deepEqual(await hiddenColumns(), ['Choice', 'D', 'E']);
|
||||||
|
|
||||||
|
// Undo and check everything reverted correctly.
|
||||||
|
await gu.undo();
|
||||||
|
assert.deepEqual(await readLabels(), ['A', 'B', 'C', 'E', 'D']);
|
||||||
|
assert.equal(await elementCount('column'), 3);
|
||||||
|
assert.equal(await element('column', 1).type(), 'Field');
|
||||||
|
assert.equal(await element('column', 1).element('label').getText(), 'E');
|
||||||
|
assert.equal(await element('column', 2).type(), 'Field');
|
||||||
|
assert.equal(await element('column', 2).element('label').getText(), 'D');
|
||||||
|
assert.equal(await element('column', 3).type(), 'Placeholder');
|
||||||
|
assert.deepEqual(await hiddenColumns(), ['Choice']);
|
||||||
|
await gu.undo();
|
||||||
|
|
||||||
// There was a bug with paragraph and columns.
|
// There was a bug with paragraph and columns.
|
||||||
// Add a paragraph to first placeholder.
|
// Add a paragraph to first placeholder.
|
||||||
await element('Columns').element(`Placeholder`, 1).click();
|
await element('Columns').element(`Placeholder`, 1).click();
|
||||||
|
@ -17,6 +17,7 @@ describe('GridViewNewColumnMenu', function () {
|
|||||||
session = await gu.session().login({showTips:true});
|
session = await gu.session().login({showTips:true});
|
||||||
api = session.createHomeApi();
|
api = session.createHomeApi();
|
||||||
docId = await session.tempNewDoc(cleanup, 'ColumnMenu');
|
docId = await session.tempNewDoc(cleanup, 'ColumnMenu');
|
||||||
|
await gu.dismissBehavioralPrompts();
|
||||||
|
|
||||||
// Add a table that will be used for lookups.
|
// Add a table that will be used for lookups.
|
||||||
await gu.sendActions([
|
await gu.sendActions([
|
||||||
@ -77,6 +78,7 @@ describe('GridViewNewColumnMenu', function () {
|
|||||||
it('should show rename menu after a new column click', async function () {
|
it('should show rename menu after a new column click', async function () {
|
||||||
await clickAddColumn();
|
await clickAddColumn();
|
||||||
await driver.findWait('.test-new-columns-menu-add-new', STANDARD_WAITING_TIME).click();
|
await driver.findWait('.test-new-columns-menu-add-new', STANDARD_WAITING_TIME).click();
|
||||||
|
await gu.waitForServer();
|
||||||
await driver.findWait('.test-column-title-popup', STANDARD_WAITING_TIME, 'rename menu is not present');
|
await driver.findWait('.test-column-title-popup', STANDARD_WAITING_TIME, 'rename menu is not present');
|
||||||
await closeAddColumnMenu();
|
await closeAddColumnMenu();
|
||||||
});
|
});
|
||||||
@ -204,29 +206,28 @@ describe('GridViewNewColumnMenu', function () {
|
|||||||
await driver.findWait('.test-new-columns-menu-add-with-type-submenu', STANDARD_WAITING_TIME);
|
await driver.findWait('.test-new-columns-menu-add-with-type-submenu', STANDARD_WAITING_TIME);
|
||||||
// popup should not be showed
|
// popup should not be showed
|
||||||
assert.isFalse(await driver.find('.test-behavioral-prompt').isPresent());
|
assert.isFalse(await driver.find('.test-behavioral-prompt').isPresent());
|
||||||
await gu.disableTips(session.email);
|
|
||||||
await closeAddColumnMenu();
|
await closeAddColumnMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const option of optionsToBeDisplayed) {
|
for (const option of optionsToBeDisplayed) {
|
||||||
it(`should allow to select column type ${option.type}`, async function () {
|
it(`should allow to select column type ${option.type}`, async function () {
|
||||||
// open add new colum menu
|
// open add new colum menu
|
||||||
await clickAddColumn();
|
await clickAddColumn();
|
||||||
// select "Add Column With type" option
|
// select "Add Column With type" option
|
||||||
await driver.findWait('.test-new-columns-menu-add-with-type', STANDARD_WAITING_TIME).click();
|
await driver.findWait('.test-new-columns-menu-add-with-type', STANDARD_WAITING_TIME).click();
|
||||||
// wait for submenu to appear
|
// wait for submenu to appear
|
||||||
await driver.findWait('.test-new-columns-menu-add-with-type-submenu', STANDARD_WAITING_TIME);
|
await driver.findWait('.test-new-columns-menu-add-with-type-submenu', STANDARD_WAITING_TIME);
|
||||||
// check if it is present in the menu
|
// check if it is present in the menu
|
||||||
const element = await driver.findWait(
|
const element = await driver.findWait(
|
||||||
`.test-new-columns-menu-add-${option.testClass}`.toLowerCase(),
|
`.test-new-columns-menu-add-${option.testClass}`.toLowerCase(),
|
||||||
100,
|
100,
|
||||||
`${option.type} option is not present`);
|
`${option.type} option is not present`);
|
||||||
// click on the option and check if column is added with a proper type
|
// click on the option and check if column is added with a proper type
|
||||||
await element.click();
|
await element.click();
|
||||||
await gu.waitForServer();//discard rename menu
|
await gu.waitForServer();
|
||||||
|
//discard rename menu
|
||||||
await driver.findWait('.test-column-title-close', STANDARD_WAITING_TIME).click();
|
await driver.findWait('.test-column-title-close', STANDARD_WAITING_TIME).click();
|
||||||
//check if new column is present
|
//check if new column is present
|
||||||
|
|
||||||
await gu.selectColumn('D');
|
await gu.selectColumn('D');
|
||||||
await gu.openColumnPanel();
|
await gu.openColumnPanel();
|
||||||
const type = await gu.getType();
|
const type = await gu.getType();
|
||||||
@ -255,14 +256,11 @@ describe('GridViewNewColumnMenu', function () {
|
|||||||
`${optionsTriggeringMenu.type} option is not present`);
|
`${optionsTriggeringMenu.type} option is not present`);
|
||||||
// click on the option and check if column is added with a proper type
|
// click on the option and check if column is added with a proper type
|
||||||
await element.click();
|
await element.click();
|
||||||
|
await gu.waitForServer();
|
||||||
//discard rename menu
|
//discard rename menu
|
||||||
await driver.findWait('.test-column-title-close', STANDARD_WAITING_TIME).click();
|
await driver.findWait('.test-column-title-close', STANDARD_WAITING_TIME).click();
|
||||||
await gu.waitForServer();
|
//check if right menu is opened on column section
|
||||||
//check if left menu is opened on column section
|
|
||||||
assert.isTrue(await driver.findWait('.test-right-tab-field', 1000).isDisplayed());
|
assert.isTrue(await driver.findWait('.test-right-tab-field', 1000).isDisplayed());
|
||||||
|
|
||||||
await gu.disableTips(session.email);
|
|
||||||
await gu.dismissBehavioralPrompts();
|
|
||||||
await gu.toggleSidePanel("right", "close");
|
await gu.toggleSidePanel("right", "close");
|
||||||
await gu.undo(1);
|
await gu.undo(1);
|
||||||
});
|
});
|
||||||
@ -287,9 +285,9 @@ describe('GridViewNewColumnMenu', function () {
|
|||||||
`${optionsTriggeringMenu.type} option is not present`);
|
`${optionsTriggeringMenu.type} option is not present`);
|
||||||
// click on the option and check if column is added with a proper type
|
// click on the option and check if column is added with a proper type
|
||||||
await element.click();
|
await element.click();
|
||||||
|
await gu.waitForServer();
|
||||||
//discard rename menu
|
//discard rename menu
|
||||||
await driver.findWait('.test-column-title-close', STANDARD_WAITING_TIME).click();
|
await driver.findWait('.test-column-title-close', STANDARD_WAITING_TIME).click();
|
||||||
await gu.waitForServer();
|
|
||||||
//check if referenceColumnsConfig is present
|
//check if referenceColumnsConfig is present
|
||||||
await gu.waitToPass(async ()=> assert.isTrue(
|
await gu.waitToPass(async ()=> assert.isTrue(
|
||||||
await driver.findContentWait(
|
await driver.findContentWait(
|
||||||
@ -299,8 +297,6 @@ describe('GridViewNewColumnMenu', function () {
|
|||||||
).isDisplayed()
|
).isDisplayed()
|
||||||
), 5000);
|
), 5000);
|
||||||
await gu.dismissBehavioralPrompts();
|
await gu.dismissBehavioralPrompts();
|
||||||
await gu.disableTips(session.email);
|
|
||||||
|
|
||||||
await gu.toggleSidePanel("right", "close");
|
await gu.toggleSidePanel("right", "close");
|
||||||
await gu.undo(1);
|
await gu.undo(1);
|
||||||
});
|
});
|
||||||
@ -328,14 +324,11 @@ describe('GridViewNewColumnMenu', function () {
|
|||||||
`${optionsTriggeringMenu.type} option is not present`);
|
`${optionsTriggeringMenu.type} option is not present`);
|
||||||
// click on the option and check if column is added with a proper type
|
// click on the option and check if column is added with a proper type
|
||||||
await element.click();
|
await element.click();
|
||||||
|
await gu.waitForServer();
|
||||||
//discard rename menu
|
//discard rename menu
|
||||||
await driver.findWait('.test-column-title-close', STANDARD_WAITING_TIME).click();
|
await driver.findWait('.test-column-title-close', STANDARD_WAITING_TIME).click();
|
||||||
await gu.waitForServer();
|
//check if right menu is opened on column section
|
||||||
//check if left menu is opened on column section
|
|
||||||
assert.isFalse(await driver.find('.test-right-tab-field').isPresent());
|
assert.isFalse(await driver.find('.test-right-tab-field').isPresent());
|
||||||
|
|
||||||
await gu.disableTips(session.email);
|
|
||||||
await gu.dismissBehavioralPrompts();
|
|
||||||
await gu.toggleSidePanel("right", "close");
|
await gu.toggleSidePanel("right", "close");
|
||||||
await gu.undo(1);
|
await gu.undo(1);
|
||||||
});
|
});
|
||||||
@ -381,10 +374,10 @@ describe('GridViewNewColumnMenu', function () {
|
|||||||
await clickAddColumn();
|
await clickAddColumn();
|
||||||
// select "create formula column" option
|
// select "create formula column" option
|
||||||
await driver.findWait('.test-new-columns-menu-add-formula', STANDARD_WAITING_TIME).click();
|
await driver.findWait('.test-new-columns-menu-add-formula', STANDARD_WAITING_TIME).click();
|
||||||
// there should not be a rename poup
|
|
||||||
assert.isFalse(await driver.find('test-column-title-popup').isPresent());
|
|
||||||
//check if new column is present
|
//check if new column is present
|
||||||
await gu.waitForServer();
|
await gu.waitForServer();
|
||||||
|
// there should not be a rename poup
|
||||||
|
assert.isFalse(await driver.find('test-column-title-popup').isPresent());
|
||||||
// check if editor popup is opened
|
// check if editor popup is opened
|
||||||
await driver.findWait('.test-floating-editor-popup', 200, 'Editor popup is not present');
|
await driver.findWait('.test-floating-editor-popup', 200, 'Editor popup is not present');
|
||||||
// write some formula
|
// write some formula
|
||||||
|
Loading…
Reference in New Issue
Block a user