mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(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:
@@ -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 : []);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
219
app/client/ui/TutorialCard.ts
Normal file
219
app/client/ui/TutorialCard.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -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",
|
||||
|
||||
@@ -100,8 +100,9 @@ export interface BehavioralPromptPrefs {
|
||||
* List of all popups that user can see and dismiss
|
||||
*/
|
||||
export const DismissedPopup = StringUnion(
|
||||
'deleteRecords', // confirmation for deleting records keyboard shortcut
|
||||
'deleteFields', // confirmation for deleting columns keyboard shortcut
|
||||
'deleteRecords', // confirmation for deleting records keyboard shortcut,
|
||||
'deleteFields', // confirmation for deleting columns keyboard shortcut,
|
||||
'tutorialFirstCard', // first card of the tutorial,
|
||||
);
|
||||
export type DismissedPopup = typeof DismissedPopup.type;
|
||||
|
||||
|
||||
@@ -77,6 +77,8 @@ export const commonUrls = {
|
||||
|
||||
efcrConnect: 'https://efc-r.com/connect',
|
||||
efcrHelp: 'https://www.nioxus.info/eFCR-Help',
|
||||
basicTutorial: 'https://templates.getgrist.com/woXtXUBmiN5T/Grist-Basics',
|
||||
basicTutorialImage: 'https://www.getgrist.com/wp-content/uploads/2021/08/lightweight-crm.png',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user