mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
cd0c6de53e
Summary: Extracts code from showExampleCard into a generic function which is reused for document tours. It handles reading and writing to user preferences for automatic showing and explicitly reopening. Test Plan: Manually tested that it automatically shows a tour just once and clicking to reopen works. There's not much new functionality so there's little that needs testing. This is an initial version that's mostly internal and is likely to be polished for users in the future. If I should still add tests, I'd like confirmation that the current behaviour is as desired. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2944
256 lines
9.0 KiB
TypeScript
256 lines
9.0 KiB
TypeScript
import { GristDoc } from "app/client/components/GristDoc";
|
|
import { urlState } from "app/client/models/gristUrlState";
|
|
import { showExampleCard } from 'app/client/ui/ExampleCard';
|
|
import { examples } from 'app/client/ui/ExampleInfo';
|
|
import { createHelpTools, cssSectionHeader, cssSpacer, cssTools } from 'app/client/ui/LeftPanelCommon';
|
|
import { cssLinkText, cssPageEntry, cssPageIcon, cssPageLink } from 'app/client/ui/LeftPanelCommon';
|
|
import { hoverTooltip, tooltipCloseButton } from 'app/client/ui/tooltips';
|
|
import { colors } from 'app/client/ui2018/cssVars';
|
|
import { icon } from 'app/client/ui2018/icons';
|
|
import { cssLink } from 'app/client/ui2018/links';
|
|
import { menuAnnotate } from 'app/client/ui2018/menus';
|
|
import { userOverrideParams } from 'app/common/gristUrls';
|
|
import { Disposable, dom, makeTestId, Observable, observable, styled } from "grainjs";
|
|
import {getUserOrgPrefObs} from "app/client/models/UserPrefs";
|
|
import {loadGristDoc} from "app/client/lib/imports";
|
|
|
|
const testId = makeTestId('test-tools-');
|
|
|
|
export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Observable<boolean>): Element {
|
|
const isOwner = gristDoc.docPageModel.currentDoc.get()?.access === 'owners';
|
|
const isOverridden = Boolean(gristDoc.docPageModel.userOverride.get());
|
|
const canViewAccessRules = observable(false);
|
|
function updateCanViewAccessRules() {
|
|
canViewAccessRules.set((isOwner && !isOverridden) ||
|
|
gristDoc.docModel.rules.getNumRows() > 0);
|
|
}
|
|
owner.autoDispose(gristDoc.docModel.rules.tableData.tableActionEmitter.addListener(updateCanViewAccessRules));
|
|
updateCanViewAccessRules();
|
|
return cssTools(
|
|
cssTools.cls('-collapsed', (use) => !use(leftPanelOpen)),
|
|
cssSectionHeader("TOOLS"),
|
|
|
|
cssPageEntry(
|
|
cssPageEntry.cls('-selected', (use) => use(gristDoc.activeViewId) === 'acl'),
|
|
cssPageEntry.cls('-disabled', (use) => !use(canViewAccessRules)),
|
|
dom.domComputed(canViewAccessRules, (_canViewAccessRules) => {
|
|
return cssPageLink(
|
|
cssPageIcon('EyeShow'),
|
|
cssLinkText('Access Rules',
|
|
menuAnnotate('Beta', cssBetaTag.cls(''))
|
|
),
|
|
_canViewAccessRules ? urlState().setLinkUrl({docPage: 'acl'}) : null,
|
|
isOverridden ? addRevertViewAsUI() : null,
|
|
);
|
|
}),
|
|
testId('access-rules'),
|
|
),
|
|
|
|
cssPageEntry(
|
|
cssPageLink(cssPageIcon('Log'), cssLinkText('Document History'), testId('log'),
|
|
dom.on('click', () => gristDoc.showTool('docHistory')))
|
|
),
|
|
// TODO: polish validation and add it back
|
|
dom.maybe((use) => use(gristDoc.app.features).validationsTool, () =>
|
|
cssPageEntry(
|
|
cssPageLink(cssPageIcon('Validation'), cssLinkText('Validate Data'), testId('validate'),
|
|
dom.on('click', () => gristDoc.showTool('validations'))))
|
|
),
|
|
cssPageEntry(
|
|
cssPageEntry.cls('-selected', (use) => use(gristDoc.activeViewId) === 'code'),
|
|
cssPageLink(cssPageIcon('Code'),
|
|
cssLinkText('Code View'),
|
|
urlState().setLinkUrl({docPage: 'code'})
|
|
),
|
|
testId('code'),
|
|
),
|
|
cssSpacer(),
|
|
dom.maybe(gristDoc.docPageModel.currentDoc, (doc) => {
|
|
if (!doc.workspace.isSupportWorkspace) { return null; }
|
|
const ex = examples.find((e) => e.matcher.test(doc.name));
|
|
if (!ex || !ex.tutorialUrl) { return null; }
|
|
return cssPageEntry(
|
|
cssPageLink(cssPageIcon('Page'), cssLinkText('How-to Tutorial'), testId('tutorial'),
|
|
{href: ex.tutorialUrl, target: '_blank'},
|
|
cssExampleCardOpener(
|
|
icon('TypeDetails'),
|
|
testId('welcome-opener'),
|
|
automaticHelpTool(
|
|
(info) => showExampleCard(ex, info),
|
|
gristDoc,
|
|
"seenExamples",
|
|
ex.id
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
// Shows the 'Tour of this Document' button if a GristDocTour table exists
|
|
// at the time of running. Currently doesn't observe the set of existing tables
|
|
gristDoc.docData.getTable('GristDocTour') &&
|
|
cssPageEntry(
|
|
cssPageLink(
|
|
cssPageIcon('Page'),
|
|
cssLinkText('Tour of this Document'),
|
|
testId('doctour'),
|
|
automaticHelpTool(
|
|
async ({markAsSeen}) => {
|
|
const gristDocModule = await loadGristDoc();
|
|
await gristDocModule.startDocTour(gristDoc.docData, markAsSeen);
|
|
},
|
|
gristDoc,
|
|
"seenDocTours",
|
|
gristDoc.docId()
|
|
),
|
|
),
|
|
),
|
|
createHelpTools(gristDoc.docPageModel.appModel, false)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Helper for showing users some kind of help (example cards or document tours)
|
|
* automatically if they haven't seen it before, or if they click
|
|
* on some element to explicitly show it again. Put this in said dom element,
|
|
* and it will provide the onclick handler and a handler which automatically
|
|
* shows when the dom element is attached, both by calling showFunc.
|
|
*
|
|
* prefKey is a key for a list of identifiers saved in user preferences.
|
|
* itemId should be a single identifier that fits in that list.
|
|
* If itemId is already present then the help will not be shown automatically,
|
|
* otherwise it will be added to the list and saved under prefKey
|
|
* when info.markAsSeen() is called.
|
|
*/
|
|
function automaticHelpTool(
|
|
showFunc: (info: AutomaticHelpToolInfo) => void,
|
|
gristDoc: GristDoc,
|
|
prefKey: 'seenExamples' | 'seenDocTours',
|
|
itemId: number | string
|
|
) {
|
|
function show(elem: HTMLElement, reopen: boolean) {
|
|
const appModel = gristDoc.docPageModel.appModel;
|
|
const prefObs: Observable<typeof itemId[] | undefined> = getUserOrgPrefObs(appModel, prefKey);
|
|
const seenIds = prefObs.get() || [];
|
|
|
|
// If this help was previously dismissed, don't show it again, unless the user is reopening it.
|
|
if (!reopen && seenIds.includes(itemId)) {
|
|
return;
|
|
}
|
|
|
|
// When the help is closed, if it's the first time it's dismissed, save this fact, to avoid
|
|
// showing it automatically again in the future.
|
|
function markAsSeen() {
|
|
try {
|
|
if (!seenIds.includes(itemId)) {
|
|
const seen = new Set(seenIds);
|
|
seen.add(itemId);
|
|
prefObs.set([...seen].sort());
|
|
}
|
|
} catch (e) {
|
|
// If we fail to save this preference, it's probably not worth alerting the user about,
|
|
// so just log to console.
|
|
// tslint:disable-next-line:no-console
|
|
console.warn("Failed to save userPref " + prefKey, e);
|
|
}
|
|
}
|
|
|
|
showFunc({elem, reopen, markAsSeen});
|
|
}
|
|
|
|
return [
|
|
dom.on('click', (ev, elem) => {
|
|
ev.preventDefault();
|
|
show(elem as HTMLElement, true);
|
|
}),
|
|
(elem: HTMLElement) => {
|
|
// Once the trigger element is attached to DOM, show the help
|
|
setTimeout(() => show(elem, false), 0);
|
|
}
|
|
];
|
|
}
|
|
|
|
/** Values which may be useful when showing an automatic help tool */
|
|
export interface AutomaticHelpToolInfo {
|
|
// Element where automaticHelpTool is attached, typically a button,
|
|
// which shows the help when clicked
|
|
elem: HTMLElement;
|
|
|
|
// true if the help was shown explicitly by clicking elem,
|
|
// false if it's being shown automatically to new users
|
|
reopen: boolean;
|
|
|
|
// Call this when the user explicitly dismisses the help to
|
|
// remember this in user preferences and not show it automatically on next load
|
|
markAsSeen: () => void;
|
|
}
|
|
|
|
// When viewing a page as another user, the "Access Rules" page link includes a button to revert
|
|
// the user and open the page, and a click on the page link shows a tooltip to revert.
|
|
function addRevertViewAsUI() {
|
|
return [
|
|
// A button that allows reverting back to yourself.
|
|
dom('a',
|
|
cssExampleCardOpener.cls(''),
|
|
cssRevertViewAsButton.cls(''),
|
|
icon('Convert'),
|
|
urlState().setHref(userOverrideParams(null, {docPage: 'acl'})),
|
|
dom.on('click', (ev) => ev.stopPropagation()), // Avoid refreshing the tooltip.
|
|
testId('revert-view-as'),
|
|
),
|
|
|
|
// A tooltip that allows reverting back to yourself.
|
|
hoverTooltip((ctl) =>
|
|
cssConvertTooltip(icon('Convert'),
|
|
cssLink('Return to viewing as yourself',
|
|
urlState().setHref(userOverrideParams(null, {docPage: 'acl'})),
|
|
),
|
|
tooltipCloseButton(ctl),
|
|
),
|
|
{openOnClick: true}
|
|
),
|
|
];
|
|
}
|
|
|
|
const cssConvertTooltip = styled('div', `
|
|
display: flex;
|
|
align-items: center;
|
|
--icon-color: ${colors.lightGreen};
|
|
|
|
& > .${cssLink.className} {
|
|
margin-left: 8px;
|
|
}
|
|
`);
|
|
|
|
const cssExampleCardOpener = styled('div', `
|
|
cursor: pointer;
|
|
margin-right: 4px;
|
|
margin-left: auto;
|
|
border-radius: 16px;
|
|
border-radius: 3px;
|
|
height: 24px;
|
|
width: 24px;
|
|
padding: 4px;
|
|
line-height: 0px;
|
|
--icon-color: ${colors.light};
|
|
background-color: ${colors.lightGreen};
|
|
&:hover {
|
|
background-color: ${colors.darkGreen};
|
|
}
|
|
.${cssTools.className}-collapsed & {
|
|
display: none;
|
|
}
|
|
`);
|
|
|
|
const cssRevertViewAsButton = styled(cssExampleCardOpener, `
|
|
background-color: ${colors.darkGrey};
|
|
&:hover {
|
|
background-color: ${colors.slate};
|
|
}
|
|
`);
|
|
|
|
const cssBetaTag = styled('div', `
|
|
.${cssPageEntry.className}-disabled & {
|
|
opacity: 0.4;
|
|
}
|
|
`);
|