(core) Adding tutorial card

Summary:
For now only html stub and docList adjustement for showing a tutorial card.
It will be used in future diffs after tutorial implementation.

Test Plan: Manual

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3750
This commit is contained in:
Jarosław Sadziński
2023-03-28 18:11:40 +02:00
parent 6b5c178953
commit a317a727c8
20 changed files with 425 additions and 31 deletions

View File

@@ -66,7 +66,6 @@ import {isList, isListType, isRefListType, RecalcWhen} from 'app/common/gristTyp
import {HashLink, IDocPage, isViewDocPage, SpecialDocPage, ViewDocPage} from 'app/common/gristUrls';
import {undef, waitObs} from 'app/common/gutil';
import {LocalPlugin} from "app/common/plugin";
import {DismissedPopup} from 'app/common/Prefs';
import {StringUnion} from 'app/common/StringUnion';
import {TableData} from 'app/common/TableData';
import {DocStateComparison} from 'app/common/UserAPI';
@@ -490,11 +489,6 @@ export class GristDoc extends DisposableWithEvents {
this.draftMonitor = Drafts.create(this, this);
this.cursorMonitor = CursorMonitor.create(this, this);
this.editorMonitor = EditorMonitor.create(this, this);
G.window.resetSeenPopups = (seen = false) => {
this.docPageModel.appModel.dismissedPopups.set(seen ? DismissedPopup.values : []);
};
}
/**

View File

@@ -21,7 +21,7 @@ import {getDefaultThemePrefs, Theme, ThemeAppearance, ThemeColors, ThemePrefs,
import {getThemeColors} from 'app/common/Themes';
import {getGristConfig} from 'app/common/urlUtils';
import {getOrgName, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
import {getUserPrefObs, getUserPrefsObs} from 'app/client/models/UserPrefs';
import {getUserPrefObs, getUserPrefsObs, markAsSeen, markAsUnSeen} from 'app/client/models/UserPrefs';
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
const t = makeT('AppModel');
@@ -111,6 +111,8 @@ export interface AppModel {
isSupport(): boolean; // If user is a Support user
isOwner(): boolean; // If user is an owner of this org
isOwnerOrEditor(): boolean; // If user is an owner or editor of this org
/** Creates an computed observable to dismiss a popup or check if it was dismissed */
dismissedPopup(name: DismissedPopup): Observable<boolean>;
}
export class TopAppModelImpl extends Disposable implements TopAppModel {
@@ -277,6 +279,10 @@ export class AppModelImpl extends Disposable implements AppModel {
urlState().pushUrl({createTeam: false, params: {}}, {avoidReload: true, replace: true}).catch(() => {});
this.showNewSiteModal(state.params?.planType);
}
G.window.resetSeenPopups = (seen = false) => {
this.dismissedPopups.set(seen ? DismissedPopup.values : []);
};
}
public get planName() {
@@ -337,6 +343,18 @@ export class AppModelImpl extends Disposable implements AppModel {
}
}
public dismissedPopup(name: DismissedPopup): Computed<boolean> {
const computed = Computed.create(null, use => use(this.dismissedPopups).includes(name));
computed.onWrite(value => {
if (value) {
markAsSeen(this.dismissedPopups, name);
} else {
markAsUnSeen(this.dismissedPopups, name);
}
});
return computed;
}
/**
* If the current user is a new user, record a sign-up event via Google Tag Manager.
*/

View File

@@ -98,3 +98,17 @@ export function markAsSeen<T>(seenIdsObs: Observable<T[] | undefined>, itemId: T
console.warn("Failed to save preference in markAsSeen", e);
}
}
export function markAsUnSeen<T>(seenIdsObs: Observable<T[] | undefined>, itemId: T) {
const seenIds = seenIdsObs.get() || [];
try {
if (seenIds.includes(itemId)) {
const seen = new Set(seenIds);
seen.delete(itemId);
seenIdsObs.set([...seen].sort());
}
} catch (e) {
// tslint:disable-next-line:no-console
console.warn("Failed to save preference in markAsUnSeen", e);
}
}

View File

@@ -12,6 +12,7 @@ import {attachAddNewTip} from 'app/client/ui/AddNewTip';
import * as css from 'app/client/ui/DocMenuCss';
import {buildHomeIntro, buildWorkspaceIntro} from 'app/client/ui/HomeIntro';
import {buildUpgradeButton} from 'app/client/ui/ProductUpgrades';
import {buildTutorialCard} from 'app/client/ui/TutorialCard';
import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs';
import {shadowScroll} from 'app/client/ui/shadowScroll';
import {transition} from 'app/client/ui/transitions';
@@ -71,7 +72,12 @@ function attachWelcomePopups(home: HomeModel): (el: Element) => void {
function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
const flashDocId = observable<string|null>(null);
const upgradeButton = buildUpgradeButton(owner, home.app);
return css.docList(
return css.docList( /* vbox */
/* first line */
dom.create(buildTutorialCard, { app: home.app }),
/* hbox */
css.docListContent(
/* left column - grow 1 */
css.docMenu(
attachAddNewTip(home),
@@ -169,7 +175,7 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
),
dom.maybe(use => !use(isNarrowScreenObs()) && ['all', 'workspace'].includes(use(home.currentPage)),
() => upgradeButton.showUpgradeCard(css.upgradeCard.cls(''))),
);
));
}
function buildAllDocsBlock(

View File

@@ -8,11 +8,6 @@ import {bigBasicButton} from 'app/client/ui2018/buttons';
// styles, which gives it priority.
import 'popweasel';
export const docMenu = styled('div', `
flex-grow: 1;
max-width: 100%;
`);
// The "&:after" clause forces some padding below all docs.
export const docList = styled('div', `
height: 100%;
@@ -20,6 +15,7 @@ export const docList = styled('div', `
overflow-y: auto;
position: relative;
display: flex;
flex-direction: column;
&:after {
content: "";
@@ -38,6 +34,15 @@ export const docList = styled('div', `
}
`);
export const docListContent = styled('div', `
display: flex;
`);
export const docMenu = styled('div', `
flex-grow: 1;
max-width: 100%;
`);
const listHeader = styled('div', `
min-height: 32px;
line-height: 32px;

View File

@@ -16,7 +16,7 @@ import {testId, theme} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {menu, menuIcon, menuItem, upgradableMenuItem, upgradeText} from 'app/client/ui2018/menus';
import {confirmModal} from 'app/client/ui2018/modals';
import {shouldHideUiElement} from 'app/common/gristUrls';
import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls';
import * as roles from 'app/common/roles';
import {Workspace} from 'app/common/UserAPI';
import {computed, dom, domComputed, DomElementArg, observable, Observable, styled} from 'grainjs';
@@ -111,19 +111,26 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom
cssPageEntry(
dom.hide(shouldHideUiElement("templates")),
cssPageEntry.cls('-selected', (use) => use(home.currentPage) === "templates"),
cssPageLink(cssPageIcon('FieldTable'), cssLinkText(t("Examples & Templates")),
cssPageLink(cssPageIcon('Board'), cssLinkText(t("Examples & Templates")),
urlState().setLinkUrl({homePage: "templates"}),
testId('dm-templates-page'),
),
),
cssPageEntry(
cssPageEntry.cls('-selected', (use) => use(home.currentPage) === "trash"),
cssPageLink(cssPageIcon('Remove'), cssLinkText(t("Trash")),
cssPageLink(cssPageIcon('RemoveBig'), cssLinkText(t("Trash")),
urlState().setLinkUrl({homePage: "trash"}),
testId('dm-trash'),
),
),
cssSpacer(),
cssPageEntry(
dom.hide(shouldHideUiElement("templates")),
cssPageLink(cssPageIcon('Bookmark'), cssLinkText(t("Tutorial")),
{ href: commonUrls.basicTutorial, target: '_blank' },
testId('dm-basic-tutorial'),
),
),
createVideoTourToolsButton(),
createHelpTools(home.app),
)

View File

@@ -758,7 +758,7 @@ const cssSeparator = styled('div', `
margin-top: 16px;
`);
const cssConfigContainer = styled('div', `
const cssConfigContainer = styled('div.test-config-container', `
overflow: auto;
--color-list-item: none;
--color-list-item-hover: none;

View File

@@ -0,0 +1,219 @@
import {AppModel} from 'app/client/models/AppModel';
import {bigPrimaryButton} from 'app/client/ui2018/buttons';
import {isNarrowScreenObs, theme} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls';
import {Computed, dom, IDisposableOwner, makeTestId, styled} from 'grainjs';
const testId = makeTestId('test-tutorial-card-');
interface Options {
app: AppModel,
onStart?: () => void,
}
export function buildTutorialCard(owner: IDisposableOwner, options: Options) {
const {app, onStart} = options;
const dismissed = app.dismissedPopup('tutorialFirstCard');
owner.autoDispose(dismissed);
function onClose() {
dismissed.set(true);
}
const visible = Computed.create(owner, (use) =>
!use(dismissed)
&& !use(isNarrowScreenObs())
&& !shouldHideUiElement("templates")
);
return dom.maybe(visible, () => {
return cssCard(
cssCaption(
dom('div', cssNewToGrist("New to Grist?")),
cssRelative(
cssStartHere("Start here."),
cssArrow()
),
),
cssContent(
testId('content'),
cssImage({src: commonUrls.basicTutorialImage}),
cssCardText(
cssLine(cssTitle("Grist Basics Tutorial")),
cssLine("Learn the basics of reference columns, linked widgets, column types, & cards."),
cssLine(cssSub('Beginner - 10 mins')),
cssButtonWrapper(
cssButtonWrapper.cls('-small'),
cssHeroButton("Start Tutorial"),
dom.on('click', () => onStart?.())
),
),
),
cssButtonWrapper(
cssButtonWrapper.cls('-big'),
cssHeroButton("Start Tutorial"),
{href: commonUrls.basicTutorial, target: '_blank'},
),
cssCloseButton(icon('CrossBig'), dom.on('click', () => onClose?.()), testId('close')),
);
});
}
const cssContent = styled('div', `
position: relative;
display: flex;
align-items: flex-start;
padding-top: 24px;
padding-bottom: 20px;
padding-right: 20px;
max-width: 460px;
`);
const cssCardText = styled('div', `
display: flex;
flex-direction: column;
justify-content: center;
align-self: stretch;
margin-left: 12px;
`);
const cssRelative = styled('div', `
position: relative;
`);
const cssNewToGrist = styled('span', `
font-style: normal;
font-weight: 400;
font-size: 24px;
line-height: 16px;
letter-spacing: 0.2px;
white-space: nowrap;
`);
const cssStartHere = styled('span', `
font-style: normal;
font-weight: 700;
font-size: 24px;
line-height: 16px;
letter-spacing: 0.2px;
white-space: nowrap;
`);
const cssCaption = styled('div', `
display: flex;
flex-direction: column;
gap: 12px;
margin-left: 32px;
margin-top: 42px;
margin-right: 64px;
`);
const cssTitle = styled('span', `
font-weight: 600;
font-size: 20px;
`);
const cssSub = styled('span', `
font-size: 12px;
color: ${theme.lightText};
`);
const cssLine = styled('div', `
margin-bottom: 6px;
`);
const cssHeroButton = styled(bigPrimaryButton, `
`);
const cssButtonWrapper = styled('a', `
flex-grow: 1;
display: flex;
justify-content: flex-end;
margin-right: 60px;
align-items: center;
text-decoration: none;
&:hover {
text-decoration: none;
}
&-big .${cssHeroButton.className} {
padding: 16px 28px;
font-weight: 600;
font-size: 20px;
line-height: 1em;
}
`);
const cssCloseButton = styled('div', `
flex-shrink: 0;
align-self: flex-end;
cursor: pointer;
--icon-color: ${theme.controlSecondaryFg};
margin: 8px 8px 4px 0px;
padding: 2px;
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
&:hover {
background-color: ${theme.lightHover};
}
&:active {
background-color: ${theme.hover};
}
`);
const cssImage = styled('img', `
width: 187px;
height: 145px;
flex: none;
`);
const cssArrow = styled('div', `
position: absolute;
background-image: var(--icon-GreenArrow);
width: 94px;
height: 12px;
top: calc(50% - 6px);
left: calc(100% - 12px);
z-index: 1;
`);
const cssCard = styled('div', `
display: flex;
position: relative;
color: ${theme.text};
border-radius: 3px;
margin-bottom: 24px;
max-width: 1000px;
box-shadow: 0 2px 18px 0 ${theme.modalInnerShadow}, 0 0 1px 0 ${theme.modalOuterShadow};
& .${cssButtonWrapper.className}-small {
display: none;
}
@media (max-width: 1320px) {
& .${cssButtonWrapper.className}-small {
flex-direction: column;
display: flex;
margin-top: 14px;
align-self: flex-start;
}
& .${cssButtonWrapper.className}-big {
display: none;
}
}
@media (max-width: 1000px) {
& .${cssArrow.className} {
display: none;
}
& .${cssCaption.className} {
flex-direction: row;
margin-bottom: 24px;
}
& {
flex-direction: column;
}
& .${cssContent.className} {
padding: 12px;
max-width: 100%;
margin-bottom: 28px;
}
}
`);

View File

@@ -39,6 +39,8 @@ export type IconName = "ChartArea" |
"AddUser" |
"BarcodeQR" |
"BarcodeQR2" |
"Board" |
"Bookmark" |
"CenterAlign" |
"Chat" |
"Code" |
@@ -68,6 +70,7 @@ export type IconName = "ChartArea" |
"FontStrikethrough" |
"FontUnderline" |
"FunctionResult" |
"GreenArrow" |
"Grow" |
"Help" |
"Home" |
@@ -105,6 +108,7 @@ export type IconName = "ChartArea" |
"PublicFilled" |
"Redo" |
"Remove" |
"RemoveBig" |
"Repl" |
"ResizePanel" |
"Revert" |
@@ -176,6 +180,8 @@ export const IconList: IconName[] = ["ChartArea",
"AddUser",
"BarcodeQR",
"BarcodeQR2",
"Board",
"Bookmark",
"CenterAlign",
"Chat",
"Code",
@@ -205,6 +211,7 @@ export const IconList: IconName[] = ["ChartArea",
"FontStrikethrough",
"FontUnderline",
"FunctionResult",
"GreenArrow",
"Grow",
"Help",
"Home",
@@ -242,6 +249,7 @@ export const IconList: IconName[] = ["ChartArea",
"PublicFilled",
"Redo",
"Remove",
"RemoveBig",
"Repl",
"ResizePanel",
"Revert",