(core) Add April Fools easter egg

Summary: What happens when you type "rr" instead of "r" in an anchor link's row number?

Test Plan: Browser tests.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D3829
This commit is contained in:
George Gevoian 2023-03-27 13:25:25 -04:00
parent f59c1edf16
commit 01fbe871aa
10 changed files with 397 additions and 59 deletions

View File

@ -9,8 +9,16 @@ import {Computed, Disposable, dom, Observable} from 'grainjs';
import {IPopupOptions} from 'popweasel'; import {IPopupOptions} from 'popweasel';
export interface AttachOptions { export interface AttachOptions {
/** Defaults to false. */
forceShow?: boolean;
/** Defaults to false. */ /** Defaults to false. */
hideArrow?: boolean; hideArrow?: boolean;
/** Defaults to false. */
hideDontShowTips?: boolean;
/** Defaults to true. */
markAsSeen?: boolean;
/** Defaults to false. */
showOnMobile?: boolean;
popupOptions?: IPopupOptions; popupOptions?: IPopupOptions;
onDispose?(): void; onDispose?(): void;
} }
@ -60,11 +68,11 @@ export class BehavioralPromptsManager extends Disposable {
// Don't show tips if surveying is disabled. // Don't show tips if surveying is disabled.
// TODO: Move this into a dedicated variable - this is only a short-term fix for hiding // TODO: Move this into a dedicated variable - this is only a short-term fix for hiding
// tips in grist-core. // tips in grist-core.
!getGristConfig().survey || (!getGristConfig().survey && prompt !== 'rickRow') ||
// Or on mobile - the design currently isn't mobile-friendly. // Or if this tip shouldn't be shown on mobile.
isNarrowScreen() || (isNarrowScreen() && !options.showOnMobile) ||
// Or if "Don't show tips" was checked in the past. // Or if "Don't show tips" was checked in the past.
this._prefs.get().dontShowTips || (this._prefs.get().dontShowTips && !options.forceShow) ||
// Or if this tip has been shown and dismissed in the past. // Or if this tip has been shown and dismissed in the past.
this.hasSeenTip(prompt) this.hasSeenTip(prompt)
) { ) {
@ -88,15 +96,16 @@ export class BehavioralPromptsManager extends Disposable {
} }
}; };
const {hideArrow = false, onDispose, popupOptions} = options; const {hideArrow, hideDontShowTips, markAsSeen = true, onDispose, popupOptions} = options;
const {title, content} = GristBehavioralPrompts[prompt]; const {title, content} = GristBehavioralPrompts[prompt];
const ctl = showBehavioralPrompt(refElement, title(), content(), { const ctl = showBehavioralPrompt(refElement, title(), content(), {
onClose: (dontShowTips) => { onClose: (dontShowTips) => {
if (dontShowTips) { this._dontShowTips(); } if (dontShowTips) { this._dontShowTips(); }
this._markAsSeen(prompt); if (markAsSeen) { this._markAsSeen(prompt); }
}, },
hideArrow, hideArrow,
popupOptions, popupOptions,
hideDontShowTips,
}); });
ctl.onDispose(() => { ctl.onDispose(() => {

View File

@ -36,7 +36,7 @@ import {DocData} from 'app/client/models/DocData';
import {DocInfoRec, DocModel, ViewFieldRec, ViewRec, ViewSectionRec} from 'app/client/models/DocModel'; import {DocInfoRec, DocModel, ViewFieldRec, ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
import {DocPageModel} from 'app/client/models/DocPageModel'; import {DocPageModel} from 'app/client/models/DocPageModel';
import {UserError} from 'app/client/models/errors'; import {UserError} from 'app/client/models/errors';
import {urlState} from 'app/client/models/gristUrlState'; import {getMainOrgUrl, urlState} from 'app/client/models/gristUrlState';
import {getFilterFunc, QuerySetManager} from 'app/client/models/QuerySet'; import {getFilterFunc, QuerySetManager} from 'app/client/models/QuerySet';
import {getUserOrgPrefObs, getUserOrgPrefsObs, markAsSeen} from 'app/client/models/UserPrefs'; import {getUserOrgPrefObs, getUserOrgPrefsObs, markAsSeen} from 'app/client/models/UserPrefs';
import {App} from 'app/client/ui/App'; import {App} from 'app/client/ui/App';
@ -48,8 +48,10 @@ import {IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
import {linkFromId, selectBy} from 'app/client/ui/selectBy'; import {linkFromId, selectBy} from 'app/client/ui/selectBy';
import {startWelcomeTour} from 'app/client/ui/welcomeTour'; import {startWelcomeTour} from 'app/client/ui/welcomeTour';
import {IWidgetType} from 'app/client/ui/widgetTypes'; import {IWidgetType} from 'app/client/ui/widgetTypes';
import {isNarrowScreen, mediaSmall, testId} from 'app/client/ui2018/cssVars'; import {PlayerState, YouTubePlayer} from 'app/client/ui/YouTubePlayer';
import {isNarrowScreen, mediaSmall, mediaXSmall, testId, theme} from 'app/client/ui2018/cssVars';
import {IconName} from 'app/client/ui2018/IconList'; import {IconName} from 'app/client/ui2018/IconList';
import {icon} from 'app/client/ui2018/icons';
import {invokePrompt} from 'app/client/ui2018/modals'; import {invokePrompt} from 'app/client/ui2018/modals';
import {FieldEditor} from "app/client/widgets/FieldEditor"; import {FieldEditor} from "app/client/widgets/FieldEditor";
import {DiscussionPanel} from 'app/client/widgets/DiscussionEditor'; import {DiscussionPanel} from 'app/client/widgets/DiscussionEditor';
@ -78,6 +80,7 @@ import {
Holder, Holder,
IDisposable, IDisposable,
IDomComponent, IDomComponent,
keyframes,
Observable, Observable,
styled, styled,
subscribe, subscribe,
@ -87,6 +90,8 @@ import * as ko from 'knockout';
import cloneDeepWith = require('lodash/cloneDeepWith'); import cloneDeepWith = require('lodash/cloneDeepWith');
import isEqual = require('lodash/isEqual'); import isEqual = require('lodash/isEqual');
const RICK_ROLL_YOUTUBE_EMBED_ID = 'dQw4w9WgXcQ';
const t = makeT('GristDoc'); const t = makeT('GristDoc');
const G = getBrowserGlobals('document', 'window'); const G = getBrowserGlobals('document', 'window');
@ -190,6 +195,9 @@ export class GristDoc extends DisposableWithEvents {
private _rawSectionOptions: Observable<RawSectionOptions|null> = Observable.create(this, null); private _rawSectionOptions: Observable<RawSectionOptions|null> = Observable.create(this, null);
private _activeContent: Computed<IDocPage|RawSectionOptions>; private _activeContent: Computed<IDocPage|RawSectionOptions>;
private _docTutorialHolder = Holder.create<DocTutorial>(this); private _docTutorialHolder = Holder.create<DocTutorial>(this);
private _isRickRowing: Observable<boolean> = Observable.create(this, false);
private _showBackgroundVideoPlayer: Observable<boolean> = Observable.create(this, false);
private _backgroundVideoPlayerHolder: Holder<YouTubePlayer> = Holder.create(this);
constructor( constructor(
@ -280,6 +288,40 @@ export class GristDoc extends DisposableWithEvents {
const cursorPos = this._getCursorPosFromHash(state.hash); const cursorPos = this._getCursorPosFromHash(state.hash);
await this.recursiveMoveToCursorPos(cursorPos, true); await this.recursiveMoveToCursorPos(cursorPos, true);
} }
if (state.hash.rickRow && !this._isRickRowing.get()) {
YouTubePlayer.create(this._backgroundVideoPlayerHolder, RICK_ROLL_YOUTUBE_EMBED_ID, {
height: '100%',
width: '100%',
origin: getMainOrgUrl(),
playerVars: {
controls: 0,
disablekb: 1,
fs: 0,
iv_load_policy: 3,
modestbranding: 1,
},
onPlayerStateChange: (_player, event) => {
if (event.data === PlayerState.Playing) {
this._isRickRowing.set(true);
}
},
}, cssYouTubePlayer.cls(''));
this._showBackgroundVideoPlayer.set(true);
this._waitForView()
.then(() => {
const cursor = document.querySelector('.selected_cursor.active_cursor');
if (cursor) {
this.behavioralPromptsManager.showTip(cursor, 'rickRow', {
forceShow: true,
hideDontShowTips: true,
markAsSeen: false,
showOnMobile: true,
onDispose: () => this.playRickRollVideo(),
});
}
})
.catch(reportError);
}
} catch (e) { } catch (e) {
reportError(e); reportError(e);
} finally { } finally {
@ -482,6 +524,14 @@ export class GristDoc extends DisposableWithEvents {
return cssViewContentPane( return cssViewContentPane(
testId('gristdoc'), testId('gristdoc'),
cssViewContentPane.cls("-contents", isPopup), cssViewContentPane.cls("-contents", isPopup),
dom.maybe(this._isRickRowing, () => cssStopRickRowingButton(
cssCloseIcon('CrossBig'),
dom.on('click', () => {
this._isRickRowing.set(false);
this._showBackgroundVideoPlayer.set(false);
}),
testId('gristdoc-stop-rick-rowing'),
)),
dom.domComputed(this._activeContent, (content) => { dom.domComputed(this._activeContent, (content) => {
return ( return (
content === 'code' ? dom.create(CodeEditorPanel, this) : content === 'code' ? dom.create(CodeEditorPanel, this) :
@ -504,6 +554,13 @@ export class GristDoc extends DisposableWithEvents {
}) })
); );
}), }),
dom.maybe(this._showBackgroundVideoPlayer, () => [
cssBackgroundVideo(
this._backgroundVideoPlayerHolder.get()?.buildDom(),
cssBackgroundVideo.cls('-fade-in-and-out', this._isRickRowing),
testId('gristdoc-background-video'),
),
]),
); );
} }
@ -1123,6 +1180,41 @@ export class GristDoc extends DisposableWithEvents {
} }
} }
/**
* Starts playing the music video for Never Gonna Give You Up in the background.
*/
public async playRickRollVideo() {
const backgroundVideoPlayer = this._backgroundVideoPlayerHolder.get();
if (!backgroundVideoPlayer) { return; }
await backgroundVideoPlayer.isLoaded();
backgroundVideoPlayer.play();
const setVolume = async (start: number, end: number, step: number) => {
let volume: number;
const condition = start <= end
? () => volume <= end
: () => volume >= end;
const afterthought = start <= end
? () => volume += step
: () => volume -= step;
for (volume = start; condition(); afterthought()) {
backgroundVideoPlayer.setVolume(volume);
await delay(250);
}
};
await setVolume(0, 100, 5);
await delay(190 * 1000);
if (!this._isRickRowing.get()) { return; }
await setVolume(100, 0, 5);
this._isRickRowing.set(false);
this._showBackgroundVideoPlayer.set(false);
}
/** /**
* Waits for a view to be ready * Waits for a view to be ready
*/ */
@ -1448,3 +1540,63 @@ const cssViewContentPane = styled('div', `
overflow: hidden; overflow: hidden;
} }
`); `);
const fadeInAndOut = keyframes(`
0% {
opacity: 0.01;
}
5%, 95% {
opacity: 0.2;
}
100% {
opacity: 0.01;
}
`);
const cssBackgroundVideo = styled('div', `
position: fixed;
top: 0;
right: 0;
height: 100%;
width: 100%;
opacity: 0;
pointer-events: none;
&-fade-in-and-out {
animation: ${fadeInAndOut} 200s;
}
`);
const cssYouTubePlayer = styled('div', `
position: absolute;
width: 450%;
height: 450%;
top: -175%;
left: -175%;
@media ${mediaXSmall} {
& {
width: 450%;
height: 450%;
top: -175%;
left: -175%;
}
}
`);
const cssStopRickRowingButton = styled('div', `
position: fixed;
top: 0;
right: 0;
padding: 8px;
margin: 16px;
border-radius: 24px;
background-color: ${theme.toastBg};
cursor: pointer;
`);
const cssCloseIcon = styled(icon, `
height: 24px;
width: 24px;
--icon-color: ${theme.toastControlFg};
`);

View File

@ -4,7 +4,7 @@ import {FocusLayer} from 'app/client/lib/FocusLayer';
import {reportSuccess} from 'app/client/models/errors'; import {reportSuccess} from 'app/client/models/errors';
import {basicButton, bigPrimaryButton, primaryButton} from 'app/client/ui2018/buttons'; import {basicButton, bigPrimaryButton, primaryButton} from 'app/client/ui2018/buttons';
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox'; import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
import {testId, theme, vars} from 'app/client/ui2018/cssVars'; import {mediaXSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons'; import {icon} from 'app/client/ui2018/icons';
import {cssModalTooltip, modalTooltip} from 'app/client/ui2018/modals'; import {cssModalTooltip, modalTooltip} from 'app/client/ui2018/modals';
import {dom, DomContents, keyframes, observable, styled, svg} from 'grainjs'; import {dom, DomContents, keyframes, observable, styled, svg} from 'grainjs';
@ -138,6 +138,8 @@ export interface ShowBehavioralPromptOptions {
onClose: (dontShowTips: boolean) => void; onClose: (dontShowTips: boolean) => void;
/** Defaults to false. */ /** Defaults to false. */
hideArrow?: boolean; hideArrow?: boolean;
/** Defaults to false. */
hideDontShowTips?: boolean;
popupOptions?: IPopupOptions; popupOptions?: IPopupOptions;
} }
@ -147,7 +149,7 @@ export function showBehavioralPrompt(
content: DomContents, content: DomContents,
options: ShowBehavioralPromptOptions options: ShowBehavioralPromptOptions
) { ) {
const {onClose, hideArrow, popupOptions} = options; const {onClose, hideArrow = false, hideDontShowTips = false, popupOptions} = options;
const arrow = hideArrow ? null : buildArrow(); const arrow = hideArrow ? null : buildArrow();
const dontShowTips = observable(false); const dontShowTips = observable(false);
const tooltip = modalTooltip(refElement, const tooltip = modalTooltip(refElement,
@ -180,6 +182,7 @@ export function showBehavioralPrompt(
cssSkipTipsCheckboxLabel("Don't show tips"), cssSkipTipsCheckboxLabel("Don't show tips"),
testId('behavioral-prompt-dont-show-tips') testId('behavioral-prompt-dont-show-tips')
), ),
dom.style('visibility', hideDontShowTips ? 'hidden' : ''),
), ),
cssDismissPromptButton('Got it', testId('behavioral-prompt-dismiss'), cssDismissPromptButton('Got it', testId('behavioral-prompt-dismiss'),
dom.on('click', () => { onClose(dontShowTips.get()); ctl.close(); }) dom.on('click', () => { onClose(dontShowTips.get()); ctl.close(); })
@ -194,6 +197,10 @@ export function showBehavioralPrompt(
offset: { offset: {
offset: '0,12', offset: '0,12',
}, },
preventOverflow: {
boundariesElement: 'window',
padding: 32,
},
} }
}) })
); );
@ -340,6 +347,12 @@ const cssBehavioralPromptModal = styled('div', `
&[x-placement^=right] { &[x-placement^=right] {
animation-name: ${cssFadeInFromRight}; animation-name: ${cssFadeInFromRight};
} }
@media ${mediaXSmall} {
& {
width: 320px;
}
}
`); `);
const cssBehavioralPromptContainer = styled(cssTheme, ` const cssBehavioralPromptContainer = styled(cssTheme, `

View File

@ -1,9 +1,11 @@
import * as commands from 'app/client/components/commands';
import {makeT} from 'app/client/lib/localization';
import {ShortcutKey, ShortcutKeyContent} from 'app/client/ui/ShortcutKey';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links'; import {cssLink} from 'app/client/ui2018/links';
import {commonUrls} from 'app/common/gristUrls'; import {commonUrls} from 'app/common/gristUrls';
import {BehavioralPrompt} from 'app/common/Prefs'; import {BehavioralPrompt} from 'app/common/Prefs';
import {dom, DomContents, DomElementArg, styled} from 'grainjs'; import {dom, DomContents, DomElementArg, styled} from 'grainjs';
import {icon} from 'app/client/ui2018/icons';
import {makeT} from 'app/client/lib/localization';
const t = makeT('GristTooltips'); const t = makeT('GristTooltips');
@ -202,4 +204,18 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
...args, ...args,
), ),
}, },
rickRow: {
title: () => t('Anchor Links'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div',
t('To make an anchor link that takes the user to a specific cell, click on'
+ ' a row and press {{shortcut}}.',
{
shortcut: ShortcutKey(ShortcutKeyContent(commands.allCommands.copyLink.humanKeys[0])),
}
),
),
...args,
),
}
}; };

View File

@ -1,16 +1,20 @@
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 {getMainOrgUrl} from 'app/client/models/gristUrlState';
import {cssLinkText, cssPageEntryMain, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon'; import {cssLinkText, cssPageEntryMain, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon';
import {YouTubePlayer} from 'app/client/ui/YouTubePlayer';
import {theme} from 'app/client/ui2018/cssVars'; import {theme} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons'; import {icon} from 'app/client/ui2018/icons';
import {cssModalCloseButton, modal} from 'app/client/ui2018/modals'; import {cssModalCloseButton, modal} from 'app/client/ui2018/modals';
import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls'; import {shouldHideUiElement} from 'app/common/gristUrls';
import {dom, makeTestId, styled} from 'grainjs'; import {dom, makeTestId, styled} from 'grainjs';
const t = makeT('OpenVideoTour'); const t = makeT('OpenVideoTour');
const testId = makeTestId('test-video-tour-'); const testId = makeTestId('test-video-tour-');
const VIDEO_TOUR_YOUTUBE_EMBED_ID = 'qnr2Pfnxdlc';
/** /**
* Opens a modal containing a video tour of Grist. * Opens a modal containing a video tour of Grist.
*/ */
@ -24,15 +28,16 @@ const testId = makeTestId('test-video-tour-');
dom.on('click', () => ctl.close()), dom.on('click', () => ctl.close()),
testId('close'), testId('close'),
), ),
cssVideoWrap( cssYouTubePlayerContainer(
cssVideo( dom.create(YouTubePlayer,
VIDEO_TOUR_YOUTUBE_EMBED_ID,
{ {
src: commonUrls.videoTour, onPlayerReady: (player) => player.playVideo(),
title: t("YouTube video player"), height: '100%',
frameborder: '0', width: '100%',
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture', origin: getMainOrgUrl(),
allowfullscreen: '',
}, },
cssYouTubePlayer.cls(''),
), ),
), ),
testId('modal'), testId('modal'),
@ -94,18 +99,16 @@ const cssModal = styled('div', `
max-width: 864px; max-width: 864px;
`); `);
const cssVideoWrap = styled('div', ` const cssYouTubePlayerContainer = styled('div', `
position: relative; position: relative;
padding-bottom: 56.25%; padding-bottom: 56.25%;
height: 0; height: 0;
`); `);
const cssVideo = styled('iframe', ` const cssYouTubePlayer = styled('div', `
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%;
height: 100%;
`); `);
const cssVideoTourTextButton = styled('div', ` const cssVideoTourTextButton = styled('div', `

View File

@ -0,0 +1,25 @@
import {theme} from 'app/client/ui2018/cssVars';
import {styled} from 'grainjs';
export const ShortcutKeyContent = styled('span', `
font-style: normal;
font-family: inherit;
color: ${theme.shortcutKeyPrimaryFg};
`);
export const ShortcutKeyContentStrong = styled(ShortcutKeyContent, `
font-weight: 700;
`);
export const ShortcutKey = styled('div', `
display: inline-block;
padding: 2px 5px;
border-radius: 4px;
margin: 0px 2px;
border: 1px solid ${theme.shortcutKeyBorder};
color: ${theme.shortcutKeyFg};
background-color: ${theme.shortcutKeyBg};
font-family: inherit;
font-style: normal;
white-space: nowrap;
`);

View File

@ -0,0 +1,115 @@
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
import {waitObs} from 'app/common/gutil';
import {Disposable, dom, DomElementArg} from 'grainjs';
import ko from 'knockout';
export interface Player {
playVideo(): void;
pauseVideo(): void;
stopVideo(): void;
mute(): void;
unMute(): void;
setVolume(volume: number): void;
}
export interface PlayerOptions {
height?: string;
width?: string;
origin?: string;
playerVars?: PlayerVars;
onPlayerReady?(player: Player): void
onPlayerStateChange?(player: Player, event: PlayerStateChangeEvent): void;
}
export interface PlayerVars {
controls?: 0 | 1;
disablekb?: 0 | 1;
fs?: 0 | 1;
iv_load_policy?: 1 | 3;
modestbranding?: 0 | 1;
}
export interface PlayerStateChangeEvent {
data: PlayerState;
}
export enum PlayerState {
Unstarted = -1,
Ended = 0,
Playing = 1,
Paused = 2,
Buffering = 3,
VideoCued = 5,
}
const G = getBrowserGlobals('document', 'window');
/**
* Wrapper component for the YouTube IFrame Player API.
*
* Fetches the JavaScript code for the API if needed, and creates an iframe that
* points to a YouTube video with the specified id.
*
* For more documentation, see https://developers.google.com/youtube/iframe_api_reference.
*/
export class YouTubePlayer extends Disposable {
private _domArgs: DomElementArg[];
private _isLoading: ko.Observable<boolean> = ko.observable(true);
private _playerId = `youtube-player-${this._videoId}`;
private _player: Player;
constructor(
private _videoId: string,
private _options: PlayerOptions,
...domArgs: DomElementArg[]
) {
super();
this._domArgs = domArgs;
if (!G.window.YT) {
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag?.parentNode?.insertBefore(tag, firstScriptTag);
G.window.onYouTubeIframeAPIReady = () => this._handleYouTubeIframeAPIReady();
} else {
setTimeout(() => this._handleYouTubeIframeAPIReady(), 0);
}
}
public isLoaded() {
return waitObs(this._isLoading, (val) => !val);
}
public play() {
this._player.playVideo();
}
public setVolume(volume: number) {
this._player.setVolume(volume);
}
public buildDom() {
return dom('div', {id: this._playerId}, ...this._domArgs);
}
private _handleYouTubeIframeAPIReady() {
const {onPlayerReady, onPlayerStateChange, playerVars, ...otherOptions} = this._options;
this._player = new G.window.YT.Player(this._playerId, {
videoId: this._videoId,
playerVars,
events: {
onReady: () => {
this._isLoading(false);
onPlayerReady?.(this._player);
},
onStateChange: (event: PlayerStateChangeEvent) =>
onPlayerStateChange?.(this._player, event),
},
...otherOptions,
});
}
}

View File

@ -2,6 +2,7 @@ import { makeT } from 'app/client/lib/localization';
import * as commands from 'app/client/components/commands'; import * as commands from 'app/client/components/commands';
import { urlState } from 'app/client/models/gristUrlState'; import { urlState } from 'app/client/models/gristUrlState';
import { IOnBoardingMsg, startOnBoarding } from "app/client/ui/OnBoardingPopups"; import { IOnBoardingMsg, startOnBoarding } from "app/client/ui/OnBoardingPopups";
import { ShortcutKey, ShortcutKeyContent } from 'app/client/ui/ShortcutKey';
import { theme } from 'app/client/ui2018/cssVars'; import { theme } from 'app/client/ui2018/cssVars';
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";
@ -14,8 +15,12 @@ export const welcomeTour: IOnBoardingMsg[] = [
title: t('Editing Data'), title: t('Editing Data'),
body: () => [ body: () => [
dom('p', dom('p',
t('Double-click or hit {{enter}} on a cell to edit it. ', {enter: Key(KeyContent(t('Enter')))}), t('Double-click or hit {{enter}} on a cell to edit it. ', {
t('Start with {{equal}} to enter a formula.', { equal: Key(KeyStrong('=')) })) enter: ShortcutKey(ShortcutKeyContent(t('Enter'))),
}),
t('Start with {{equal}} to enter a formula.', {
equal: ShortcutKey(ShortcutKeyContent('=')),
})),
], ],
selector: '.field_clip', selector: '.field_clip',
placement: 'bottom', placement: 'bottom',
@ -39,8 +44,9 @@ export const welcomeTour: IOnBoardingMsg[] = [
dom('p', dom('p',
t('Set formatting options, formulas, or column types, such as dates, choices, or attachments. ')), t('Set formatting options, formulas, or column types, such as dates, choices, or attachments. ')),
dom('p', dom('p',
t('Make it relational! Use the {{ref}} type to link tables. ', {ref: Key(t('Reference'))}), t('Make it relational! Use the {{ref}} type to link tables. ', {
) ref: ShortcutKey(t('Reference')),
})),
], ],
placement: 'right', placement: 'right',
}, },
@ -48,7 +54,9 @@ export const welcomeTour: IOnBoardingMsg[] = [
selector: '.tour-add-new', selector: '.tour-add-new',
title: t('Building up'), title: t('Building up'),
body: () => [ body: () => [
dom('p', t('Use {{addNew}} to add widgets, pages, or import more data. ', {addNew: Key(t('Add New'))})) dom('p', t('Use {{addNew}} to add widgets, pages, or import more data. ', {
addNew: ShortcutKey(t('Add New')),
})),
], ],
placement: 'right', placement: 'right',
}, },
@ -67,7 +75,7 @@ export const welcomeTour: IOnBoardingMsg[] = [
title: t('Flying higher'), title: t('Flying higher'),
body: () => [ body: () => [
dom('p', t('Use {{helpCenter}} for documentation or questions.', dom('p', t('Use {{helpCenter}} for documentation or questions.',
{helpCenter: Key(GreyIcon('Help'), t('Help Center'))})), {helpCenter: ShortcutKey(GreyIcon('Help'), t('Help Center'))}))
], ],
placement: 'right', placement: 'right',
}, },
@ -92,29 +100,6 @@ export function startWelcomeTour(onFinishCB: () => void) {
startOnBoarding(welcomeTour, onFinishCB); startOnBoarding(welcomeTour, onFinishCB);
} }
const KeyContent = styled('span', `
font-style: normal;
font-family: inherit;
color: ${theme.shortcutKeyPrimaryFg};
`);
const KeyStrong = styled(KeyContent, `
font-weight: 700;
`);
const Key = styled('div', `
display: inline-block;
padding: 2px 5px;
border-radius: 4px;
margin: 0px 2px;
border: 1px solid ${theme.shortcutKeyBorder};
color: ${theme.shortcutKeyFg};
background-color: ${theme.shortcutKeyBg};
font-family: inherit;
font-style: normal;
white-space: nowrap;
`);
const TopBarButtonIcon = styled(icon, ` const TopBarButtonIcon = styled(icon, `
--icon-color: ${theme.topBarButtonPrimaryFg}; --icon-color: ${theme.topBarButtonPrimaryFg};
`); `);

View File

@ -85,6 +85,7 @@ export const BehavioralPrompt = StringUnion(
'pageWidgetPickerSelectBy', 'pageWidgetPickerSelectBy',
'editCardLayout', 'editCardLayout',
'addNew', 'addNew',
'rickRow',
); );
export type BehavioralPrompt = typeof BehavioralPrompt.type; export type BehavioralPrompt = typeof BehavioralPrompt.type;

View File

@ -77,7 +77,6 @@ export const commonUrls = {
efcrConnect: 'https://efc-r.com/connect', efcrConnect: 'https://efc-r.com/connect',
efcrHelp: 'https://www.nioxus.info/eFCR-Help', efcrHelp: 'https://www.nioxus.info/eFCR-Help',
videoTour: 'https://www.youtube.com/embed/qnr2Pfnxdlc?autoplay=1',
}; };
/** /**
@ -261,7 +260,11 @@ export function encodeUrl(gristConfig: Partial<GristLoadConfig>,
const hash = state.hash; const hash = state.hash;
hashParts.push(state.hash?.popup ? 'a2' : `a1`); hashParts.push(state.hash?.popup ? 'a2' : `a1`);
for (const key of ['sectionId', 'rowId', 'colRef'] as Array<keyof HashLink>) { for (const key of ['sectionId', 'rowId', 'colRef'] as Array<keyof HashLink>) {
if (hash[key]) { hashParts.push(`${key[0]}${hash[key]}`); } const partValue = hash[key];
if (partValue) {
const partKey = key === 'rowId' && state.hash?.rickRow ? 'rr' : key[0];
hashParts.push(`${partKey}${partValue}`);
}
} }
} }
const queryStr = encodeQueryParams(queryParams); const queryStr = encodeQueryParams(queryParams);
@ -379,13 +382,28 @@ export function decodeUrl(gristConfig: Partial<GristLoadConfig>, location: Locat
const hashParts = hash.split('.'); const hashParts = hash.split('.');
const hashMap = new Map<string, string>(); const hashMap = new Map<string, string>();
for (const part of hashParts) { for (const part of hashParts) {
hashMap.set(part.slice(0, 1), part.slice(1)); if (part.startsWith('rr')) {
hashMap.set(part.slice(0, 2), part.slice(2));
} else {
hashMap.set(part.slice(0, 1), part.slice(1));
}
} }
if (hashMap.has('#') && ['a1', 'a2'].includes(hashMap.get('#') || '')) { if (hashMap.has('#') && ['a1', 'a2'].includes(hashMap.get('#') || '')) {
const link: HashLink = {}; const link: HashLink = {};
for (const key of ['sectionId', 'rowId', 'colRef'] as Array<Exclude<keyof HashLink, 'popup'>>) { const keys = [
const ch = key.substr(0, 1); 'sectionId',
if (!hashMap.has(ch)) { continue; } 'rowId',
'colRef',
] as Array<Exclude<keyof HashLink, 'popup' | 'rickRow'>>;
for (const key of keys) {
let ch: string;
if (key === 'rowId' && hashMap.has('rr')) {
ch = 'rr';
link.rickRow = true;
} else {
ch = key.substr(0, 1);
if (!hashMap.has(ch)) { continue; }
}
const value = hashMap.get(ch); const value = hashMap.get(ch);
if (key === 'rowId' && value === 'new') { if (key === 'rowId' && value === 'new') {
link[key] = 'new'; link[key] = 'new';
@ -819,6 +837,7 @@ export interface HashLink {
rowId?: UIRowId; rowId?: UIRowId;
colRef?: number; colRef?: number;
popup?: boolean; popup?: boolean;
rickRow?: boolean;
} }
// Check whether a urlId is a prefix of the docId, and adequately long to be // Check whether a urlId is a prefix of the docId, and adequately long to be