mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
24fc3a2d00
Summary: The old Examples and Templates workspace is now a page that pulls templates from a new public Grist Templates org. The All Documents view will pull featured templates from that org, where featured templates are simply pinned documents in Grist Templates. The Examples and Templates page will also show the featured templates, as well as the rest of the available templates organized by category. The categories are equivalent to workspaces in Grist Templates, and are generated dynamically. Test Plan: Browser tests. Reviewers: paulfitz, dsagal Reviewed By: paulfitz, dsagal Subscribers: dsagal, paulfitz, jarek Differential Revision: https://phab.getgrist.com/D2930
255 lines
8.9 KiB
TypeScript
255 lines
8.9 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) => {
|
|
const ex = examples.find(e => e.urlId === doc.urlId);
|
|
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;
|
|
}
|
|
`);
|