mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add a button and a tooltip to Access Rules page item, in View-As mode.
Summary: - When in View-As mode, clicking the Access Rules page now shows a tooltip with a link to return to normal mode and open the Access Rules page. - A "revert" button is shown next to the item with the same behavior. - Implemented hoverTooltip() with various options. (It will have other uses.) - Simplify creation of links based on UrlState: - Allow merging with previous urlState using a function - Add a helper function to merge in aclAsUser parameter. - Add setHref() method to UrlState Test Plan: Added test cases: - for tooltips generally in test/projects - for updating UrlState using a callback - for Access Rules tooltip and button behavior Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2749
This commit is contained in:
@@ -131,19 +131,6 @@ export const cssPageLink = styled('a', `
|
||||
}
|
||||
`);
|
||||
|
||||
// Styled like a cssPageLink, but in a disabled mode, without an actual link.
|
||||
export const cssPageDisabledLink = styled('span', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
padding-left: 24px;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
color: inherit;
|
||||
`);
|
||||
|
||||
export const cssLinkText = styled('span', `
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -3,9 +3,12 @@ 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, cssPageDisabledLink, cssPageEntry, cssPageIcon, cssPageLink } 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 { userOverrideParams } from 'app/common/gristUrls';
|
||||
import { Disposable, dom, makeTestId, Observable, styled } from "grainjs";
|
||||
|
||||
const testId = makeTestId('test-tools-');
|
||||
@@ -22,10 +25,11 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
|
||||
(aclUIEnabled ?
|
||||
cssPageEntry(
|
||||
cssPageEntry.cls('-selected', (use) => use(gristDoc.activeViewId) === 'acl'),
|
||||
cssPageEntry.cls('-disabled', !canUseAccessRules),
|
||||
(canUseAccessRules ? cssPageLink : cssPageDisabledLink)(cssPageIcon('EyeShow'),
|
||||
cssPageEntry.cls('-disabled', !isOwner),
|
||||
cssPageLink(cssPageIcon('EyeShow'),
|
||||
cssLinkText('Access Rules'),
|
||||
canUseAccessRules ? urlState().setLinkUrl({docPage: 'acl'}) : null
|
||||
canUseAccessRules ? urlState().setLinkUrl({docPage: 'acl'}) : null,
|
||||
isOverridden ? addRevertViewAsUI() : null,
|
||||
),
|
||||
testId('access-rules'),
|
||||
) :
|
||||
@@ -83,6 +87,43 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -98,4 +139,14 @@ const cssExampleCardOpener = styled('div', `
|
||||
&:hover {
|
||||
background-color: ${colors.darkGreen};
|
||||
}
|
||||
.${cssTools.className}-collapsed & {
|
||||
display: none;
|
||||
}
|
||||
`);
|
||||
|
||||
const cssRevertViewAsButton = styled(cssExampleCardOpener, `
|
||||
background-color: ${colors.darkGrey};
|
||||
&:hover {
|
||||
background-color: ${colors.slate};
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -44,7 +44,6 @@ export function createTopBarDoc(owner: MultiHolder, appModel: AppModel, pageMode
|
||||
docNameSave: renameDoc,
|
||||
pageNameSave: getRenamePageFn(gristDoc),
|
||||
cancelRecoveryMode: getCancelRecoveryModeFn(gristDoc),
|
||||
cancelUserOverride: getCancelUserOverrideFn(gristDoc),
|
||||
isPageNameReadOnly: (use) => use(gristDoc.isReadonly) || typeof use(gristDoc.activeViewId) !== 'number',
|
||||
isDocNameReadOnly: (use) => use(gristDoc.isReadonly) || use(pageModel.isFork),
|
||||
isFork: pageModel.isFork,
|
||||
@@ -105,15 +104,6 @@ function getCancelRecoveryModeFn(gristDoc: GristDoc): () => Promise<void> {
|
||||
};
|
||||
}
|
||||
|
||||
function getCancelUserOverrideFn(gristDoc: GristDoc): () => Promise<void> {
|
||||
return async () => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('aclAsUser_');
|
||||
url.searchParams.delete('aclAsUserId_');
|
||||
window.location.assign(url.href);
|
||||
};
|
||||
}
|
||||
|
||||
function topBarUndoBtn(iconName: IconName, ...domArgs: DomElementArg[]): Element {
|
||||
return cssHoverCircle(
|
||||
cssTopBarUndoBtn(iconName),
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
|
||||
import {prepareForTransition} from 'app/client/ui/transitions';
|
||||
import {testId} from 'app/client/ui2018/cssVars';
|
||||
import {dom, DomContents, styled} from 'grainjs';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {dom, DomContents, DomElementMethod, styled} from 'grainjs';
|
||||
import Popper from 'popper.js';
|
||||
|
||||
export interface ITipOptions {
|
||||
@@ -24,8 +25,27 @@ export interface ITransientTipOptions extends ITipOptions {
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface IHoverTipOptions extends ITransientTipOptions {
|
||||
// How soon after mouseenter to show it. Defaults to 100 ms.
|
||||
openDelay?: number;
|
||||
|
||||
// If set and non-zero, remove the tip automatically after this time.
|
||||
timeoutMs?: number;
|
||||
|
||||
// How soon after mouseleave to hide it. Defaults to 400 ms. It also gives the pointer some time
|
||||
// to be outside of the trigger and the tooltip content if the user moves the pointer from one
|
||||
// to the other.
|
||||
closeDelay?: number;
|
||||
|
||||
// Also show the tip on clicking the element it's attached to.
|
||||
openOnClick?: boolean;
|
||||
}
|
||||
|
||||
export type ITooltipContentFunc = (ctl: ITooltipControl) => DomContents;
|
||||
|
||||
export interface ITooltipControl {
|
||||
close(): void;
|
||||
getDom(): HTMLElement; // The tooltip DOM.
|
||||
}
|
||||
|
||||
// Map of open tooltips, mapping the key (from ITipOptions) to ITooltipControl that allows
|
||||
@@ -50,7 +70,7 @@ export function showTransientTooltip(refElem: Element, tipContent: DomContents,
|
||||
* See also ITipOptions.
|
||||
*/
|
||||
export function showTooltip(
|
||||
refElem: Element, tipContent: (ctl: ITooltipControl) => DomContents, options: ITipOptions = {}
|
||||
refElem: Element, tipContent: ITooltipContentFunc, options: ITipOptions = {}
|
||||
): ITooltipControl {
|
||||
const placement: Popper.Placement = options.placement || 'top';
|
||||
const key = options.key;
|
||||
@@ -65,10 +85,10 @@ export function showTooltip(
|
||||
content.remove();
|
||||
if (key) { openTooltips.delete(key); }
|
||||
}
|
||||
const ctl: ITooltipControl = {close};
|
||||
const ctl: ITooltipControl = {close, getDom: () => content};
|
||||
|
||||
// Add the content element.
|
||||
const content = cssTooltip({role: 'tooltip'}, tipContent(ctl), testId(`transient-tooltip`));
|
||||
const content = cssTooltip({role: 'tooltip'}, tipContent(ctl), testId(`tooltip`));
|
||||
document.body.appendChild(content);
|
||||
|
||||
// Create a popper for positioning the tooltip content relative to refElem.
|
||||
@@ -86,11 +106,83 @@ export function showTooltip(
|
||||
return ctl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a tooltip on hover. Suitable for use during dom construction, e.g.
|
||||
* dom('div', 'Trigger', hoverTooltip(() => 'Hello!'))
|
||||
*/
|
||||
export function hoverTooltip(tipContent: ITooltipContentFunc, options?: IHoverTipOptions): DomElementMethod {
|
||||
return (elem) => setHoverTooltip(elem, tipContent, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a tooltip to the given element, to be rendered on hover.
|
||||
*/
|
||||
export function setHoverTooltip(refElem: Element, tipContent: ITooltipContentFunc, options: IHoverTipOptions = {}) {
|
||||
const {openDelay = 100, timeoutMs, closeDelay = 400} = options;
|
||||
|
||||
// Controller for closing the tooltip, if one is open.
|
||||
let tipControl: ITooltipControl|undefined;
|
||||
|
||||
// Timer to open or close the tooltip, depending on whether tipControl is set.
|
||||
let timer: ReturnType<typeof setTimeout>|undefined;
|
||||
|
||||
function clearTimer() {
|
||||
if (timer) { clearTimeout(timer); timer = undefined; }
|
||||
}
|
||||
function resetTimer(func: () => void, delay: number) {
|
||||
clearTimer();
|
||||
timer = setTimeout(func, delay);
|
||||
}
|
||||
function scheduleCloseIfOpen() {
|
||||
clearTimer();
|
||||
if (tipControl) { resetTimer(close, closeDelay); }
|
||||
}
|
||||
function open() {
|
||||
clearTimer();
|
||||
tipControl = showTooltip(refElem, ctl => tipContent({...ctl, close}), options);
|
||||
dom.onElem(tipControl.getDom(), 'mouseenter', clearTimer);
|
||||
dom.onElem(tipControl.getDom(), 'mouseleave', scheduleCloseIfOpen);
|
||||
if (timeoutMs) { resetTimer(close, timeoutMs); }
|
||||
}
|
||||
function close() {
|
||||
clearTimer();
|
||||
tipControl?.close();
|
||||
tipControl = undefined;
|
||||
}
|
||||
|
||||
// We simulate hover effect by handling mouseenter/mouseleave.
|
||||
dom.onElem(refElem, 'mouseenter', () => {
|
||||
if (!tipControl && !timer) {
|
||||
resetTimer(open, openDelay);
|
||||
} else if (tipControl) {
|
||||
// Already shown, reset to newly-shown state.
|
||||
clearTimer();
|
||||
if (timeoutMs) { resetTimer(close, timeoutMs); }
|
||||
}
|
||||
});
|
||||
|
||||
dom.onElem(refElem, 'mouseleave', scheduleCloseIfOpen);
|
||||
|
||||
if (options.openOnClick) {
|
||||
// If request, re-open on click.
|
||||
dom.onElem(refElem, 'click', () => { close(); open(); });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a handy button for closing a tooltip.
|
||||
*/
|
||||
export function tooltipCloseButton(ctl: ITooltipControl): HTMLElement {
|
||||
return cssTooltipCloseButton(icon('CrossSmall'),
|
||||
dom.on('click', () => ctl.close()),
|
||||
testId('tooltip-close'),
|
||||
);
|
||||
}
|
||||
|
||||
const cssTooltip = styled('div', `
|
||||
position: absolute;
|
||||
z-index: 5000; /* should be higher than a modal */
|
||||
background-color: black;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 2px rgba(0,0,0,0.5);
|
||||
text-align: center;
|
||||
@@ -100,6 +192,22 @@ const cssTooltip = styled('div', `
|
||||
font-size: 10pt;
|
||||
padding: 8px 16px;
|
||||
margin: 4px;
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.2s;
|
||||
`);
|
||||
|
||||
const cssTooltipCloseButton = styled('div', `
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
margin: -4px -4px -4px 8px;
|
||||
--icon-color: white;
|
||||
border-radius: 16px;
|
||||
|
||||
&:hover {
|
||||
background-color: white;
|
||||
--icon-color: black;
|
||||
}
|
||||
`);
|
||||
|
||||
Reference in New Issue
Block a user